はじめに
Javaでまぁ、JUnitでテスト書きますよね。
テスト書くときにassertまでの間にインスタンス生成やら、
初期化やらset/getやらゴチャゴチャ書いてしまって見にくいな…って思ったことありませんか?
ということで、そこらへんを考えながらKotlinでテスト用のDSLを作ってみました。
setup、exercise、verify
JUnitでテストするにあたって、僕としてはJUnit実践入門の教えを守りたくなります。
基本的にテストは
・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でDSLを考える
ということで、なんか頭の中にあるイメージを具現化したいなぁ
と思って思い付いたのが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 件のコメント:
こんばんは。
型に忠実にやろうと思うと無駄も出てきやすいですね。
コメントを投稿