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 件のコメント:

  1. やっぱり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)
    }
    }
    }

    返信削除
    返信

    1. >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 }

      削除