2015/07/01 このエントリーをはてなブックマークに追加 はてなブックマーク - Kotlinの雑なテスト用DSLを作りなおした

Kotlinの雑なテスト用DSLを作りなおした

カテゴリ: ,






Kotlinでテスト用の雑なDSLを作るというのをやってみたんですが、
なんかイマイチなものが出来たなぁと思います。

記事を書いてからも改善出来ないかやり方を考えてたんですが、
もうちょっとマシなものが出来た気がするので、今回は続編記事です(まだ洗練されてませんが)。











前回もあげていましたが

・型のキャストをテスト実装者にさせる必要がある
・各ブロックでの情報の受け渡しが手続き的
など問題点がありました。





JetBrainsの@yanex_ruさんから
type-safe builderを利用するのとかどうですか?と教えていただいたのと、
ジェネリクスで頑張れそうだなとか思って作ってみました。





ソースは以下のような感じです。


import org.junit.Test
import kotlin.test.*
import java.util.*
import kotlin.properties.Delegates


///////////////////////////////////////////////////////////////////////////
// テスト対象
///////////////////////////////////////////////////////////////////////////

fun sum(a : IntArray) = a.sum()

///////////////////////////////////////////////////////////////////////////
// DSL
///////////////////////////////////////////////////////////////////////////
class Argument<T> (value : T) {
    val value = value
}
class TestContext<T> {
    var args : Array<Argument<*>> by Delegates.notNull()
    var expected : T by Delegates.notNull()
    var actual : T by Delegates.notNull()
    fun <T> Array<Argument<*>>.first() = this.get(0).value as T
    fun <T> Array<Argument<*>>.second() = this.drop(1).first()
    fun <T> Array<Argument<*>>.third() = this.drop(2).first()
}
class DslSetup<T>{
    val setupReciever = TestContext<T>()
    fun setup(recieve : TestContext<T>.() -> Unit) : DslExercise<T> {
        setupReciever.recieve()
        return DslExercise(setupReciever)
    }
}
class DslExercise<T>(reciever : TestContext<T>) {
    val exerciseReciever = reciever
    fun exercise(recieve : TestContext<T>.() -> Unit) : DslVerify<T> {
        exerciseReciever.recieve()
        return DslVerify(exerciseReciever)
    }
}
class DslVerify<T>(reciever : TestContext<T>) {
    val verifyReciever = reciever
    fun verify(recieve : TestContext<T>.() -> Unit) = verifyReciever.recieve()
}


///////////////////////////////////////////////////////////////////////////
// テスト
///////////////////////////////////////////////////////////////////////////


public class Tests {
    @Test
    fun testSum() {
        val test = DslSetup<Int>()
        test.setup {
            args = arrayOf(
                Argument<IntArray>(intArrayOf(1,2,3))
            )
        }.exercise {
            actual = sum(args.first())
        }.verify {
            expected = 6
            assertEquals(expected, actual)
        }
    }

}


TestContextというクラスの拡張関数型の関数を引数に持つ関数を使うことで
情報を柔軟に受け渡す事が出来ました。

これはKotlinのWebフレームワークのKaraのHtmlBuilderと近いアプローチです。

引数の関数をレシーバーとなるTestContextインスタンスで受けとることで
情報の受け渡しを可能にしています。

また各クラスにジェネリクスで型指定することで
Any型(Javaで言うところのObject型)を使うことをなるべく避けました。
ただargs.firstなどは内部でキャストしちゃってるんですけどね…。

Kotlinのスマートキャストを使って
うまくリターン出来ないかと考えてたんですけど、書き方がよく分からず(^-^;


これでsetup、exercise、verifyブロックの
それぞれで同じコンテキストを持つ事が出来ました。


僕の思想としては、テスト対象やテスト対象のメソッド引数、
依存するものの初期化やセットなどはsetupで、
exerciseでは対象の実行のみ
verifyではアサーションのみ、という方針です。





@yanex_ruさんから
からの指摘として、
Delegates.notNull()を使ったフィールド変数の委譲はあんまり推奨しないと言われました。

確かに、値の初期化がなされているか、
コンパイルレベルでは分からなくなってしまいます。

今回は他の良い方法が思い付かなかったので、そのままとしていますが
たとえばexpectedに値を代入せずにアサーションを行おうとすると実行時例外が発生してしまいます。

これはコンパイルレベルで解決できる問題かもしれません。



また、基本的にnull非許容型を使っているので、
actualやexpectedにnullを代入したい場合はどうすんの?とかってところもあります。
そこはちょっと悩み中です。







雑なDSLをちょっと改善してみよう、という感じで書いてみました。


Kotlinの良さとかちょっとは出たかなぁ。。。
テストは前回のDSLよりはやりやすくなったのでは…?!


なんかもっとこういうのあるじゃん!
とかいうご意見お待ちしております!笑







2 件のコメント:

ngsw_taro さんのコメント...

やっぱりKotlinでDSL作るのって面白いですよね!
こんなの考えました!!

import kotlin.test.assertEquals
import org.junit.Test as test

///////////////////////////////////////////////////////////////////////////
// テスト対象
///////////////////////////////////////////////////////////////////////////

fun sum(a: IntArray) = a.sum()

///////////////////////////////////////////////////////////////////////////
// DSL
///////////////////////////////////////////////////////////////////////////

class Function1Test(val target: (Param1) -> Return) {
var param1: Param1 = null

fun verify(verifier: (Return) -> Unit) {
val got = target(param1)
verifier(got)
}
}

fun ((P1) -> R).setup(setuper: Function1Test.() -> Unit): Function1Test =
Function1Test(this).let { it.setuper(); it }

///////////////////////////////////////////////////////////////////////////
// テスト
///////////////////////////////////////////////////////////////////////////

public class Tests {
test fun testSum() {
::sum setup {
param1 = intArrayOf(1, 2, 3)
} verify {
assertEquals(6, it)
}
}
}

yy_yank(やんく) さんのコメント...


>ngsw_taroさん
おお、かっこいいですね!


ちなみに、DSLの部分がコンパイル通らなかったんですが、
こんな感じですかね?
多分、ジェネリクスの部分が
エスケープされずにBlogger側で除去されてるようです。。。


class Function1Test<Param1, Return>(val target: (Param1) -> Return) {
var param1: Param1 = null
fun verify(verifier: (Return) -> Unit) {
val got = target(param1)
verifier(got)
}
}

fun <P1, R>((P1) -> R).setup(setuper: Function1Test.() -> Unit): Function1Test = Function1Test(this).let { it.setuper(); it }

コメントを投稿

GA