Theoriesについて
Theoriesランナーについて先日、私自身は使っていないことを書いたのだが、自分で作ってみると興味が出てくるものだ。 Haskellでいうquickcheckが念頭にあると、なんでこういう仕様でこういうものを作ろうとしたのかが分かる。ようだ。
quickcheckというのは、SUTがある特性(property)を満たしていることを総当りでテストしてくれる機能だ。というかそのようだ。
が、Theoriesと同じように組み合わせ爆発の問題は逃れられない。
なんにしてもJCUnitとはだいぶ思想というか方向性が違う。
テスト自動生成ツールを作ろうとすると、次の問題に行き当たる。
- なにを元にテストを作る?(どこから仕様を得る?)
- 組み合わせ爆発にどう対処する?
- テストオラクルはどうする?
FSM/JCUnitは、 1に対してユーザに入力値の組を定義させる、あるいはFSMをJavaのオブジェクトとして定義させることで、2については組み合わせテスト(Pairwise/n-wise)のテクニックを使うことで、3に対してはユーザが不変条件をJavaプログラムとして表現したり、JCUnitがFSMの定義から導出される挙動と照合することで対処している。 また、JCUnit(というか各種の組み合わせテストアルゴリズム)が生成するテストスイートは多くの場合、非常に小さいので(2値のパラメタが100個ある場合、18テストケースが出力される)仕様の定式化が困難な場合、人間がレビューした実行結果を期待値として使用すると言った使い方も可能だろう。
一方Theories的なアプローチだと、1.についてはユーザが定義したパラメタの組に対して総当りであり得べき組み合わせをすべて実施する。したがって2.についてはそういうものだと素朴に受け入れる。3.についてはユーザがプログラムとして定義した不変条件・SUTの特性そのものがテストオラクルと言える。
「素朴に受け入れる」と言ってもquickcheckはもう少し洗練された方法も持っている。ひとつは乱数に基づいてテストを選択すること、もうひとつはshrinkと呼ばれる機構だ。
どうも日本語、英語ともにわかりやすい説明が見つらない。困ったものだ。
Mapの変更
コードのある場所でTreeMapをLinkedHashMapに変えてみた。
すると、
benchmark1_3$4 :(testcases, remainders, time(sec))=( 9, 0, 0.0)
benchmark2_3$13 :(testcases, remainders, time(sec))=( 24, 0, 0.03)
benchmark3_4$15_3$17_2$20 :(testcases, remainders, time(sec))=( 39, 0, 3.053)
benchmark4_4$1_3$30_2$35 :(testcases, remainders, time(sec))=( 30, 0, 6.654)
benchmark5_2$100 :(testcases, remainders, time(sec))=( 18, 0, 17.904)
benchmark6_10$20 :(testcases, remainders, time(sec))=( 244, 0, 2.58)
以前、行った比較は以下で
横に並べてみるとこうなる
| # | Task | TreeMap | LinkedHashMap |
|---|---|---|---|
| 1 | 3(4) | 9 | 9 |
| 2 | 3(13) | 25 | 24 |
| 3 | 4(15)+3(17)+2(20) | 41 | 39 |
| 4 | 4(1)+3(30)+2(35) | 31 | 30 |
| 5 | 2(100) | 18 | 18 |
| 6 | 10(20) | 273 | 244 |
わずかながらテストスイートのサイズが小さくなっている。 原因はいろいろ考えられるが、興味深いことだ。
問題がないことを確信できたら、LinkedHashMapに変更することにしよう。
Builderのフィールドは全部publicでいいのでは
Builderがクラス化された生成子なら、それは本来メソッドの中で使い捨てにされるものだろう。 ならば、フィールドをprivateにする必要はあるのだろうか?
メソッド間、クラス間で本当に受け渡ししないならばprivateでもpublicでもいいのだが、受け渡しをするとしたらどういう時だろう?
それはあるオブジェクト構築の責任をメソッド間やクラス間で分担する場合だ。 メソッドAとメソッドBの間で構築の責任を分担しているのなら、メソッドAがBuilderに行った結果がメソッドBに影響を及ぼすことはあってもいいのではないか。 publicにすることのデメリットは、Builderはその性質上フィールドにfinal修飾子を与えられないことだ。だからメソッドBの中でBuilderの内部状態が壊されるかもしれない。
しかし壊されて困るようなフィールドならBuilderの生成子でfinalなフィールドに代入しておくべきだろう。
この記事に目を止めてくれた人のコメントをお待ちしたい。
IntelliJはParameterizedテストランナーを特別扱いする
@Suiteのついたテストクラスはまとめていろんなクラスにあるテストを実行する。このいろんなクラスというのはSuiteの「子」としてJUnitに扱われる。
@Suiteとアノテーションがつけられたクラスの中のあるメソッドをIntelliJのテスト結果画面から選択すると、選ばれたメソッドが属するクラスの選ばれたメソッドを実行する。このとき、@Suiteがついたクラス(親側のクラス)はもう関係ない。
このアプローチがうまく行くのは「子」が静的なクラスだからだ。
@RunWith(Suite.class)
@Suite.SuiteClasses({ExampleSuite.Test1.class, ExampleSuite.Test2.class})
public class ExampleSuite {
public static class Test1 {
ためしに、こういのを作ってみるといい。static修飾子をTest1から取り去るとテストが動かなくなってしまうのがわかる。
ところでParameterizedもSuiteの一種なのだ。 だが、通常のSuiteと異なり、
// Parameterized.Parameters
new Object[]{
{"test1", "hello", "world"}, // 0
{"test2", "scott", "tiger"} // 1
}
のような二重配列の中の一個一個の配列(=テストケース)例えば{"test1", "hello", "world"}が「子」に対応するだ。これらからParameterized#createRunnersForChildrenによってテストランナーが生成され、Parameterizedオブジェクトに登録される。
そして、これを一個一個別の静的クラスにすることはできない。動的に静的なクラスを作ることはできませんよね?
IntelliJは@Parameterizedテストのメソッドを個別に実行するように要求されると、実行クラスはSuiteとなる親のクラス(つまり@Parameterizedがついたクラス)、メソッドは指定されたものを実行する。
これだけでは一体、何番目のテストケースを実行するのかわからない。
ではどうするか?プログラム引数として[0]とか[1]とかを渡すのだ。
これがJUnitの側で解釈されて何番目のテストケースが実行されるかが決まる。
なるほど。
で、JCUnit。JCUnitのランナーはこのParameterizedランナーの仕組みに"則って"作られている。"則って"というのは真似して、という意味だ。Parameterizedを継承していないのだ
だから、JCUnitのテスト結果画面から、テストメソッドを一個だけ選んで実行しようとしても動かない。IntelliJがParameterizedだと認識してくれないから、プログラム引数に[1]とか[10]とかをくれないのだ。
Parameterized#createRunnersForChildrenをオーバーライドすればそれで仕事は終わりなのに、なぜか?
createRunnersForChildrenがprivateだからだ。ふざけんな。
じゃ、どうするか?ずいぶん前にぐちゃぐちゃ工夫したのだが、実行したいテストメソッド名がtestMethodだとしたらIntelliJの実行ダイアログでメソッド名の欄をtestMethod[10]とかに変更してみてほしい。10番目のテストが実行されるはずだ。
しかし、ブログは書いてみるもので、アタマが整理されてうまいインチキを思いついた。 試してみてうまく行ったら、後日報告することにしたい。
FSM/JCUnit+Selenium
これは、、、使えるぞ。
Theories runner
皆さん、JUnitのTheories runnerは使っていますか? 僕は使っていません。
可能な値の組み合わせをすべて<直積>でテストしようとするのでテストケース数が爆発してしまうのと、 一つのテストメソッドに対してそれらの組み合わせを一つのテストケースとして実行してしまうので、 問題が生じた時に追跡・局所化が難しいと思うからです。
まあそうは言っても、JUnitに最初からバンドルされてるランナーだし、爆発してしまうテストケースを 上手に減らすのはJCUnitの存在意義なので、今日はTheoriesランナーをJCUnitで使う方法を考えてみた。
Theoriesのサポートは次回のリリース(0.5.6)に含まれることになる。
@RunWith(TheoriesWithJCUnit.class)
public class TheoriesExample1 {
@DataPoints("posInt")
public static int[] positiveIntegers() {
return new int[] {
1, 2, 3
};
}
@Theory
public void test1(
int a,
int b,
int c,
int d
) throws Exception {
System.out.printf("a=%s, b=%s, c=%d, d=%d%n", a, b, c, d);
}
}
この例のソースはJCUnitのレポジトリの0.5.x-developブランチにすでにおいてある。
さて、それぞれ3つの値を持つ4つのパラメタだ。3 x 3 x 3 x 3=81通りのパターンを実行するのが通常のTheoriesランナー。 しかし、TheoriesWithJCUnitが出力するのは以下の9通りだ。
a=1, b=1, c=3, d=3
a=1, b=2, c=2, d=1
a=1, b=3, c=1, d=2
a=2, b=1, c=1, d=1
a=2, b=2, c=3, d=2
a=2, b=3, c=2, d=3
a=3, b=1, c=2, d=2
a=3, b=2, c=1, d=3
a=3, b=3, c=3, d=1
それでいて、驚くべし、任意の2つの因子(属性)を取り出して見ると可能な組み合わせはすべて網羅されているはずだ。 制約の取り扱いなどを含むもう少し複雑な例もレポジトリにおいてあるので興味のある方は参照してみてほしい。
一点だけ注意。
この例に含まれる@TupleGenerationアノテーションはメソッドに与えることができるがスタンダードなJCUnitで使う同名のアノテーションとは別物(別パッケージにおいてある。クラスとフィールドにしか付けられない)だ。
0.6.0までの間に統合するかもしれないが、少なくとも今は違うものなので気をつけてほしい。
0.5.5リリース
なんとかかんとか、0.5.5をリリースした。 発端は
JCUnitが持っているプリミティブ用のデフォルト値を外部から取りやすいようにするというだけのことだったのだが、プラグインのインタフェースがあんまりよろしくないから変えないと、とか、この際だからRunnerもユーザが使えるようにするか、とかなってずいぶん手間取ってしまった。
MavenのCoordinateは以下のとおり。
<dependency> <groupId>com.github.dakusui</groupId> <artifactId>jcunit</artifactId> <version>0.5.5</version> </dependency>
リリースノートにも書いたが、@FactorFieldの静的インナークラスDefaultLevelsにintLevels、doubleLevels等のメソッドを作ったのでそこからJCUnitのビルトインされたデフォルト値がとれる。
ということで、フィードバックをお待ちしております。>uehajさん。
その他の変更は以下の通り。 詳しくはリリースノートを見てほしい。
- Issue-#21: Organize documentation
- Issue-#22: Expose default values of @FactorField
- Issue-#23: Separate annotation system from the other parts of JCUnit