uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

JCUnitのBuilder APIをためす

以前、以下の記事で紹介した、JCUnitの開発がすすんでいます。(開発ブログ, ソース)

uehaj.hatenablog.com

主な拡張としては、状態機械をモデリングしてテスト生成ビルダーAPIの整備などなど*1

Spockからの利用サンプルをかきなおしてみました。ビルダーAPIを使用しています。

@Grab('com.github.dakusui:jcunit:0.5.4')
@Grab('org.spockframework:spock-core:1.0-groovy-2.4')
import com.github.dakusui.jcunit.core.factor.Factors
import com.github.dakusui.jcunit.core.factor.Factor
import com.github.dakusui.jcunit.generators.TupleGenerator
import com.github.dakusui.jcunit.core.FactorField;
import com.github.dakusui.jcunit.core.tuples.Tuple;
import com.github.dakusui.jcunit.constraint.constraintmanagers.ConstraintManagerBase;
import com.github.dakusui.jcunit.constraint.ConstraintManager;
import com.github.dakusui.jcunit.exceptions.UndefinedSymbol;
import spock.lang.*

class HelloSpec extends Specification {

    static ConstraintManager closureConstraintManager(names, Closure clos) {
        return new ConstraintManagerBase() {
            @Override
            boolean check(Tuple tuple) throws UndefinedSymbol {
                Binding binding = new Binding()
                names.each {
                     if (!tuple.containsKey(it)) {
                         throw new UndefinedSymbol(it)
                     }
                     binding.setProperty(it, tuple[it])
                }
                clos.delegate = binding
                clos.call()
            }
        }
    }

    static Collection genPairwiseTestData(Map factors, Closure constraint = null) {
        def builder = new TupleGenerator.Builder()
        if (constraint != null) {
            builder = builder.setConstraintManager(closureConstraintManager(factors.keySet(), constraint))
        }
        builder.setFactors(new Factors(factors.collect{k,v -> new Factor(k, v)}))
        .build()
        .collect{ it.values() };
    }

    static intLevels =  [ 1, 0, -1, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUE ];
    static stringLevels = [
        "Hello world", "こんにちは世界", "1234567890", "ABCDEFGHIJKLMKNOPQRSTUVWXYZ",
        "abcdefghijklmnopqrstuvwxyz", "`-=~!@#\$%^&*()_+[]\\{}|;':\",./<>?", " ",
        "" ]

    @Unroll
    def "分配法則(a=#a,b=#b,c=#c)"() {
    expect:
        c*(a+b) == c*a + c*b
    where:
    [a,b,c] << genPairwiseTestData([a:intLevels,
                                    b:intLevels,
                                    c:intLevels])
    }

    @Unroll
    def test1() {
    expect:
        c*(a+b) == c*a + c*b
    where:
    [a,b,c] << genPairwiseTestData([a:intLevels,
                                    b:intLevels,
                                    c:intLevels], { return a > 0 && b != c })
    }

    @Unroll
    def test2() {
    expect:
        (a+b).size() == a.size() + b.size()
    where:
    [a,b] << genPairwiseTestData([a:stringLevels,
                                  b:stringLevels])
    }

    @Unroll
    def test3() {
    expect:
        a+b == b+a
    where:
    [a,b] << genPairwiseTestData([a:[1,2,3],
                                  b:intLevels])
    }

    @Unroll
    def test4() {
    expect:
        a == b || a != b
    where:
    [a,b] << genPairwiseTestData([a:MyBoolean.values() as List, b:MyBoolean.values() as List])
    }

}

enum MyBoolean {
    True,
    False
}

以前のようにアノテーションを使用せずとも使用できるようになりました。

気付いた点としては、

  • 各型のレベルのデフォルト値をAPIクラスからうまく参照できなかったので自前で定義しています。 (上記の static intLevels = [ 1, 0, -1, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUE ]の部分)
  • SpockのデータパイプAPIにもうちょっと柔軟性が欲しい。今の呼び出し方だとデータ指向テストの疑似変数名の指定に重複があるので、疑似変数名を取得もしくは設定できるAPIがあると良いのだが。Spock Extensionで触れるフックがあるかな。

など。楽しいです。

*1:他に、生成テストデータの保存、再生などもなされているようです。