2015/06/25 このエントリーをはてなブックマークに追加 はてなブックマーク - Kotlinでテスト用の雑なDSLを作った

Kotlinでテスト用の雑なDSLを作った

カテゴリ: ,






Javaでまぁ、JUnitでテスト書きますよね。



テスト書くときにassertまでの間にインスタンス生成やら、
初期化やらset/getやらゴチャゴチャ書いてしまって見にくいな…って思ったことありませんか?

ということで、そこらへんを考えながらKotlinでテスト用のDSLを作ってみました。





JUnitでテストするにあたって、僕としてはJUnit実践入門の教えを守りたくなります。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)


基本的にテストは
・setup(準備)
・exercise(実行)
・verify(検証)
とかに分かれますという話です。


こんな感じ。




public class SumTest {
    
    @Test
    public void testSum() {
        // setup
        Hoge hoge = new Hoge();
        hoge.setHogeHoge("hogehoge");
        hoge.setHogeFuga("hogefuga");
        hoge.setHogePiyo("hogepiyo");
        Fuga fuga = new Fuga();
        fuga.setHogeHoge("hogehoge");
        fuga.setHogeFuga("hogefuga");
        fuga.setHogePiyo("hogepiyo");
        Piyo piyo = new Piyo();
        String expected = "piyo";
        // exercise
        String actual = piyo.sum(hoge, fuga);
        // verify
        assertThat(expected, is(actual));
    }
}



こう書くと、コメントで区切られてるだけでなんの規約も無いなぁと思うわけです。
テストをうまいことsetupとexerciseとverifyで三分割したい。


だからcucumber-jvmとかspockとかそういうものがあるんでしょうけど
(というとそれもまた語弊ありそうなんですけど)。

なんか簡単にそういうもの作れないかなぁと。


※ もちろん@Beforeやら@Afterやら@Ruleとかそこらへん使ってシンプルにすることもセオリーですが。





ということで、なんか頭の中にあるイメージを具現化したいなぁ
と思って思い付いたのがKotlinだったので、Kotlinで書きました。
別にJavaでも良かったんですが。



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


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

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

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


class DslFactory {
    companion object {
        fun create() = DslSetup()
    }
}

class DslSetup{
    var expected : Any by Delegates.notNull()
    var args : Array<Any> by Delegates.notNull()
    
    fun setup(f : (self : DslSetup) -> Unit) : DslExercise {
        f(this)
        return DslExercise(expected, args)
    }
}

class DslExercise(expected : Any, args : Array<Any>) {
    var actual : Any by Delegates.notNull()
    val expected = expected
    val args = args.asSequence()
    fun exercise(f : (self : DslExercise,args : Sequence<Any>) -> Unit) : DslVerify {
        f(this, args)
        return DslVerify(actual, expected)
    }
}

class DslVerify(expected : Any, actual : Any) {
    val expected = expected
    val actual = actual
    fun verify(f : (expected : Any, actual : Any) -> Unit) = f(expected, actual)
}

// ただのまやかしにすぎない拡張関数
fun Sequence<Any>.second() = this.drop(1).first()
fun Sequence<Any>.third() = this.drop(2).first()
fun Sequence<Any>.at(i : Int)  = this.drop(i - 1).first()

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


public class Tests {
    
    val test = DslFactory.create()
    @Test
    fun testSum() {
        test.setup { target ->
            target.expected = 6
            target.args = arrayOf(intArrayOf(1,2,3))
        }.exercise { target, args ->
            val arg1 = args.first() as IntArray
            target.actual = sum(arg1)
        }.verify { expected, actual ->
            assertEquals(expected, actual)
        }
    }

}



ポイントとしては
・setup、exercise、verifyの責務を明確にする
・メソッドチェインで順序性を保(とうとし)てる
・expectedやargsを初期化していないと実行時例外が発生する(setupでの宣言を矯正する)


んで、クソダサいのがテスト対象の引数をsetupで作るんですが、
exerciseブロックではもう一度受けとり直してキャストしてるってところですね。
非常に型安全な感じもなく、手続きっぽい手口です。


ここらへんの受け渡しのダサさを回避する方法考えると
setup、exercise、verifyの順番にネストしてclosureを利用する方法があると思うんですが、
なんか可読性下がるなぁと。そこらへんspockはよく出来てる気がする。
というかアレの仕組み誰か教えてください笑





ホントに作ってみただけですが、
一応目的であったテストブロックの分割と責務の分散というか
そういうものが出来ました。

実用性があるかといえば…ですが、
Kotlinの言語の特性とかユニットテストの考え方とかの話のタネにでもなればな、と。



どうしてもspockすげえなぁという感想になる。
あとKotlinにはspekというものがあるので、
JetBrains/spek - GitHub 本格的なテスティングフレームワークみたいなぁという方はそちらを。




2 件のコメント:

師子乃 さんのコメント...
このコメントは投稿者によって削除されました。
師子乃 さんのコメント...

こんばんは。

型に忠実にやろうと思うと無駄も出てきやすいですね。

コメントを投稿

GA