LoginSignup
11
16

More than 5 years have passed since last update.

QuickCheck でテストデータを半自動生成する

Posted at

概要

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
dependencies {
    testCompile 'net.java.quickcheck:quickcheck:0.6'
}

Maven 等の場合はこちら

とりあえず動かす

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回呼び出して、生成されたオブジェクトを表示するだけの簡単なコードです。

Template
@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

最小値を指定

integers
PrimitiveGenerators.integers(7) // 最小値を指定
実行結果
[803228871]
[2126942855]
[1950900576, 1146603069]
[1122380821, 1295774766, 1614686771, 1940120700, 1356357892, 1258900587, 1020490335]

そもそもの最大値が大きすぎるので、あまり役に立つ場面が思い浮かびません。

桁数を揃える

Integer の最大桁数で統一させてみます。

integers
PrimitiveGenerators.integers(1000000000)
実行結果
1878704899
1246214129
1802857637
1564484122

最小値と最大値を指定

通常はこれを使うことになるでしょう。

integers
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つの場合

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()));
}
Stacktrace
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 があるとします。

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 です。

Generator.java
public interface Generator<T> {

    /**
     * Generates the next instance.
     * 
     * @return a newly created instance
     */
    public T next();
}

お気づきの通り、Lambda 式で定義可能です。各値はそれぞれ PrimitiveGenerators で Generator を生成します。

OriginalGenerator
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

サンプルコード

1.Test codes

Related works

1.junit-quickcheck……こちらは今年に入ってもコミットがあるので、今も継続的に開発されているようです。こっちを調べればよかった

11
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
16