はじめに
Kotlinでテスト用の雑なDSLを作るというのをやってみたんですが、
なんかイマイチなものが出来たなぁと思います。
記事を書いてからも改善出来ないかやり方を考えてたんですが、
もうちょっとマシなものが出来た気がするので、今回は続編記事です(まだ洗練されてませんが)。
前回のDSLの問題点
前回もあげていましたが
・型のキャストをテスト実装者にさせる必要がある
・各ブロックでの情報の受け渡しが手続き的
など問題点がありました。
改善アプローチ
JetBrainsの@yanex_ruさんから
type-safe builderを利用するのとかどうですか?と教えていただいたのと、
ジェネリクスで頑張れそうだなとか思って作ってみました。
改善版DSL
ソースは以下のような感じです。
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ではアサーションのみ、という方針です。
Delegates.notNull()は微妙?
@yanex_ruさんから
からの指摘として、
Delegates.notNull()を使ったフィールド変数の委譲はあんまり推奨しないと言われました。
確かに、値の初期化がなされているか、
コンパイルレベルでは分からなくなってしまいます。
今回は他の良い方法が思い付かなかったので、そのままとしていますが
たとえばexpectedに値を代入せずにアサーションを行おうとすると実行時例外が発生してしまいます。
これはコンパイルレベルで解決できる問題かもしれません。
@yanex_ru factoryはそうですね。
Delegates.notNull()が安全ではないという理由はruntimeでしか問題を発見出来ないからですか?
— やんく@社内ニート (@yy_yank) 2015, 6月 25
@yy_yank そうですね。Delegates.notNull()は、いつも!!を使うことと同じぐらいですね。
— Yan Zhulanow (@yanex_ru) 2015, 6月 25
@yanex_ru あぁ、そう言われるとなんだか良くない気がしてきました…
— やんく@社内ニート (@yy_yank) 2015, 6月 25
また、基本的にnull非許容型を使っているので、
actualやexpectedにnullを代入したい場合はどうすんの?とかってところもあります。
そこはちょっと悩み中です。
まとめ
雑なDSLをちょっと改善してみよう、という感じで書いてみました。
Kotlinの良さとかちょっとは出たかなぁ。。。
テストは前回のDSLよりはやりやすくなったのでは…?!
なんかもっとこういうのあるじゃん!
とかいうご意見お待ちしております!笑
2 件のコメント:
やっぱり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)
}
}
}
>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 }
コメントを投稿