概要
Property Based Test を実装するためのライブラリ「QuickCheck」の Java 移植版を試してみました。
Property Based Test とは
以前の渋谷Javaで @gakuzzzz さんがおっしゃっていたのによると下記のようなものだそうです。
- テストデータを半自動生成……テストデータを管理する必要がない
- すべての値について性質を満たすことをテスト
- 機能変化や仕様変化に強い
- Java にも QuickCheck ファミリーのライブラリがある
テストデータを作るのが面倒な場合や、あらゆる値を網羅的にテストしたいケースで役立つようです。なお、境界値テストのような固定の値を必要とするものは、通常通りの JUnit Test を書けばよいとのことでした。
Java の QuickCheck 実装
QuickCheck は Haskell のライブラリです。今回は Java 移植版の net.java.quickcheck を使ってみました。最終コミットが2012年で、Bitbucket 上のドキュメントも2015年以降更新されていないので、開発が止まっているようです。
導入
依存の追加
build.gradleに下記の依存を追加します。
dependencies {
testCompile 'net.java.quickcheck:quickcheck:0.6'
}
とりあえず動かす
CombinedGenerators で Collection の型を指定して、PrimitiveGenerators で要素の型を指定します。
私の見たサンプルだとどちらも static import して使っていました。サンプルとしてはどうなんだろうと思わなくもないです。
@Test
public void simple() {
final Generator<List<Integer>> generator = lists(integers());
IntStream.range(0, 4).forEach(i -> System.out.println(generator.next()));
}
実行結果はこちら
[-1473595787, -1881741363, 1002236653, 864642487]
[-1453342830, 1645866063, -1615158593]
[936501104, 1232292532, 1130963563]
[]
たまに空っぽの List を返してくるのは何なのか?と思ったら、 lists では空の List も生成されるようで、必ず中身のある List が欲しい場合には CombinedGenerators#nonEmptyLists を使えばよいらしいです。
ひな形
今回は下記のひな形を使っていろいろ試します。
generator の next() メソッドを4回呼び出して、生成されたオブジェクトを表示するだけの簡単なコードです。
@Test
public void simple() {
final Generator<?> generator = /* ここに Generator を書く */;
IntStream.range(0, 4)
.mapToObj(i -> generator.next())
.forEach(System.out::println);
}
実行環境
Java SE8 を使用します。
$ java -version
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
PrimitiveGenerators
単一の値を生成する Generator の Factory です。PrimitiveGenerators にはいろいろな型の Generator を作成するファクトリメソッドが用意されています。
integers
最小値を指定
PrimitiveGenerators.integers(7) // 最小値を指定
[803228871]
[2126942855]
[1950900576, 1146603069]
[1122380821, 1295774766, 1614686771, 1940120700, 1356357892, 1258900587, 1020490335]
そもそもの最大値が大きすぎるので、あまり役に立つ場面が思い浮かびません。
桁数を揃える
Integer の最大桁数で統一させてみます。
PrimitiveGenerators.integers(1000000000)
1878704899
1246214129
1802857637
1564484122
最小値と最大値を指定
通常はこれを使うことになるでしょう。
PrimitiveGenerators.integers(7, 10) // 最小値と最大値を指定
10
10
9
7
正の整数のみ生成
PrimitiveGenerators.positiveIntegers() を使うと、正の整数だけを生成できます。
2011848657
558263171
1621972212
291054580
ほかに Long や Double や Byte でも同様のことが可能です。
strings
下記は最大7文字までの文字列を生成させます。
PrimitiveGenerators.strings(7)
実行結果はこちら
)[d^t
5gL&e
k]tI|T!
CtHE1
およそ実用的とは言えない文字列が出来上がりました。
使用可能な文字列を制限
PrimitiveGenerators.strings("abcdefghijk")
実行結果はこちら
kc
iaeafcegaddeficjjgddeajbkcj
bijabhadejk
dcecggjjigajikjajaciiaaeh
A-Zの文字列のみを使う
PrimitiveGenerators.letterStrings() を使うことにより、 A-Zの文字列のみで文字列を生成できます。
XJUKLGorEXahQKsseOcdJ
ftgGaxfweNpyrDtrRkPOnqowfec
nVSghoRXzyBcEYjpNCo
MTDvouHmpayKMnSt
いろいろなGenerator
PrimitiveGenerators.nulls()
null だけを生成します。何に使うのかまったくわかりません。
null
null
null
null
PrimitiveGenerators.dates()
Date クラスのオブジェクトを生成します。結構使いどころあるのじゃないかと思ったところ、
Tue Jan 25 11:16:33 JST 225078784
Mon Jun 12 03:52:58 JST 286379026
Wed May 21 16:47:15 JST 199363913
Mon Jul 11 08:05:45 JST 1400664
とんでもない未来が生成されました。
実際には dates(long low, long high) を使うことになるでしょう。
final Generator<Date> generator = PrimitiveGenerators.dates(0L, System.currentTimeMillis());
Wed Mar 21 21:22:28 JST 1984
Thu Jul 11 18:35:57 JST 2013
Sat Jun 15 11:26:13 JST 2013
Wed Sep 03 07:23:46 JST 1980
Sat Jun 04 17:51:33 JST 2016
PrimitiveGenerators.fixedValues(value);
渡されたオブジェクトと同じ値のものを返します。可変長引数のものも Overload されているので複数渡せます。
1つの場合
PrimitiveGenerators.fixedValues(new Point(112, 120));
何度 next() を呼んでも同じ値が返ってきます。
java.awt.Point[x=112,y=120]
java.awt.Point[x=112,y=120]
java.awt.Point[x=112,y=120]
java.awt.Point[x=112,y=120]
複数の場合
PrimitiveGenerators.fixedValues(new Point(-44, 10), new Point(112, 120), new Point(132, 190));
毎回異なる値が返ってきます。同じ値が続けて2回返ってくることもあります。
java.awt.Point[x=132,y=190]
java.awt.Point[x=-44,y=10]
java.awt.Point[x=132,y=190]
java.awt.Point[x=112,y=120]
CombinedGenerators
CombinedGenerators では複数要素を束ねるオブジェクト(要するに配列や Collection)を生成します。
ちなみに、生成できうる値の範囲がごく小さいと、Set の生成で例外が発生します。
@Test
public void simple() {
final Generator<Set<Integer>> generator
= CombinedGenerators.nonEmptySets(PrimitiveGenerators.integers(7, 10));
IntStream.range(0, 4)
.mapToObj(i -> generator.next())
.filter(l -> !l.isEmpty())
.forEach(i -> System.out.println(generator.next()));
}
net.java.quickcheck.GeneratorException: Failed to generate another value after [100] tries (generator: net.java.quickcheck.generator.support.IntegerGenerator@9e89d68)
at net.java.quickcheck.generator.support.VetoableGenerator.next(VetoableGenerator.java:56)
at net.java.quickcheck.generator.support.ListGenerator.next(ListGenerator.java:61)
at net.java.quickcheck.generator.support.SetGenerator.next(SetGenerator.java:48)
at net.java.quickcheck.generator.support.SetGenerator.next(SetGenerator.java:25)
at jp.toastkid.verification.rxjava.QuickCheckVerification.lambda$0(QuickCheckVerification.java:20)
……省略……
Generator を作ってみる
例えば、このような Pojo があるとします。
private class Pojo {
private String name;
private int price;
public Pojo(final String name, final int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return this.name + " " + this.price;
}
}
Generator は次のような Interface です。
public interface Generator<T> {
/**
* Generates the next instance.
*
* @return a newly created instance
*/
public T next();
}
お気づきの通り、Lambda 式で定義可能です。各値はそれぞれ PrimitiveGenerators で Generator を生成します。
final Generator<Pojo> generator = () -> {
final Pojo p = new Pojo();
p.name = PrimitiveGenerators.strings("abcdefg").next();
p.price = PrimitiveGenerators.integers(1, 10).next();
return p;
};
acaaacfegggfffeggec 9
ddfbdffffafbfdgddfcc 10
gbafcedb 2
bfg 7
サンプルコード
Related works
1.junit-quickcheck……こちらは今年に入ってもコミットがあるので、今も継続的に開発されているようです。こっちを調べればよかった