諸事情により、JUnitが使えない状況でコードを書いていたのですが、書いたコードが動くかどうか確認したい衝動に駆られて、簡単な特徴もないテスティングフレームワークを作りました。
仕様
最初に書いた仕様がこれ。
public class MaybeTest extends Test { @Examination public void mapStringLengthRemainsSome() { setup(() -> some("stringLength=15")) .when(maybe -> maybe.map(String::length)) .then(equalsTo(15)); } }
そのときはこんなイメージでした。
setup(Supplier<? extends T>)
でテスト対象のオブジェクトを生成するwhen(Function<? super T, ? extends N>)
でテスト対象のオブジェクトに操作を加えていくthen
でアサートの準備equalsTo(V)
で比較を実施
なお、作っていく最中でthen
メソッドをthen(Function<? super T, ? extends V>)
に変更して、実際の値を取得するように変更して、その後のequalsTo(V)
で比較をする形に変更しました。
テストスイート
あと、テストクラスをスキャンしていくのは面倒だったし、作りこみたくなかったので、
TestSuite.run(Class<? extends Test>...)
というメソッドでテストしたいクラスだけ指定してテストするようにしました。
テスト結果の表示
ある程度出来上がった所で、テスト結果くらいは見栄え良く表示したいなと思い、
これでコンソールの文字列の色の変え方を勉強しつつ、表示用のクラスの仕様を適当に書いておきました。
で、ある程度、テスト表示用のクラスを書いた所で、最後に表示の仕方を間違えないように次のようなコメントを書いてから実装しました。
private static class SuccessPrinter extends AbstractPrinter { @Override public void print() { // クラス名(緑) // 以下繰り返し // - テスト名(緑) } } private static class FailurePrinter extends AbstractPrinter { @Override public void print() { // クラス名(赤) // 以下繰り返し // Successの場合 // - テスト名(緑) // Failureの場合 // - テスト名(赤) // diff(赤) // Accidentの場合 // - テスト名(黄) // explanation(normal) // Panicの場合 // - テスト名(黄) // cause(normal) } }
感謝
テストの実行自体をExecutorService
でクラスごとに並行して実行させたくて、かつ終了したら次の処理(テスト結果表示→テスト結果集計→集計結果表示)をやろうとして、CompletableFuture.allOf(CompletableFuture<?>...)
を使おうとしたわけですが、Stream
でRunnable
をボコボコ生成して、CompletableFuture.runAsync
に渡してCompletableFuture<Void>
を生成してて、これを配列化しようとしていた所、new CompletableFuture<Void>[size]
をしようとすると、Generic Array creationにひっかかってしまって、コンパイルエラーになるという事態が発生。
対策としてclass VoidFuture extends CompletableFuture<Void>{}
というクラスを作って、new VoidFuture[size]
をやりましたが、こんどはArrayStoreException
が発生(VoidFuture
はCompletableFuture<Void>
ではないので、配列に詰め込むことができない)。
テンパッて壁を殴って4個くらい穴を開けながらツイートしていた所、
@makingさんやうらがみさんから、いろいろと助言をいただきました。
@mike_neck thenCombine使うとやりたいことがうまくできるとかありませんかね?
— Toshiaki Maki (@making) November 20, 2015
@making @mike_neck こんなんでいけました https://t.co/UGMadqYXat が、みけさん、こういう事で合ってます?
— うらがみ (@backpaper0) November 20, 2015
結果的に、CompletableFuture.allOf(CompletableFuture<?>...)
に対して次のようにすることで、すべてのテスト結果の同期を図ることができました。
public CompletableFuture<Void> run() { return CompletableFuture.allOf(tests.stream() .map(createCases) .<Runnable>map(runners(queue)) .map(r -> CompletableFuture.runAsync(r, exec)) .collect(toList()).toArray(new CompletableFuture<?>[tests.size()])); }
new CompletableFuture<?>[size]
だとGeneric array creationに引っかからないんですね…
まあ、お二方ともご協力ありがとうございます。
できた代物
で、こんな感じのテストができました。
テストスイートにテストを指定する
public class Main { public static void main(String[] args) { TestSuite.run(NothingTest.class, SomeTest.class); } }
テストクラスそのもの
import static com.sample.data.api.Maybe.some; import static com.sample.data.api.Maybe.nothing; public class SomeTest extends Test { @Execute public void someIsSomeReturnsTrue() { setup(() -> some(20)) .then(Maybe::isSome) .equalsTo(true); } @Execute public void mapperReturnsNullOnMapItBecomesNothing() { setup(() -> some("foo")) .when(m -> m.map(s -> null)) .then(Maybe::isSome) .equalsTo(false); } @Execute public void someFmapedToNothingThenItBecomesNothing() { setup(() -> some(-5)) .when(m -> m.fmap(i -> i < 0 ? nothing() : some(i))) .then(Maybe::isSome) .equalsTo(false); } @Execute public void someNotFilteredRemainsSome() { setup(() -> some("someNotFiltered")) .when(m -> m.filter(s -> s.startsWith("some"))) .then(Maybe::isSome) .equalsTo(true); } @Execute public void someOrReturnsOriginalValue() { setup(() -> some("gene kelly")) .when(m -> m.map(CAPITALIZE)) .then(m -> m.or("Frank Sinatra")) .equalsTo("Gene Kelly"); } private static final Function<String, String> CAPITALIZE = s -> { boolean toUpper = true; char space = ' '; StringBuilder sb = new StringBuilder(); for (char c : s.toCharArray()) { sb.append(toUpper ? Character.toUpperCase(c) : c); toUpper = c == space; } return sb.toString(); }; }
テスト実行
これ、JUnitを使えない事情の原因は、このGradleプロジェクトがJVM components modelを採用していて、現状のGradle2.9ではExternal dependencyを解決できないことだったわけです。
というわけで、この簡単な特徴もないテスティングフレームワークもJVM component modelのコンポーネントの一つとして実装しています。
で、実行するためには、JVM components modelで作成される大量のjarファイルを集めてclasspathに指定しないといけないわけで、Gradleのカスタムタスクを作成して実行するように出来ています。
task dataTestExec(type: JavaExec, dependsOn: 'assemble') { classpath = files(tasks.assemble.dependsOn.findAll {it instanceof BaseBinarySpec}.collect { [it.jarFile, it.apiJarFile] }.flatten().collect {it.absolutePath}) main = 'com.sample.Main' }
で、これを実行すると次のようにテストが実行されます。
ちなみに、上の例では全部のテストが通っていますが、テスト失敗するとこんな表示になります。
と、まあ、簡単で(欝でやる気でないので2日かかりましたが…)至って何の特徴もないテスティングフレームワークが完成しました。
こいつの公開等は、JVM components modelのpublishingがまだ未実装っぽいので、できても先の話になるでしょうし、はっきり言ってJUnitやTestNG、Spockの方が明らかに高機能なので、公開するつもりはありません。
リポジトリー
ここ
おわり