方法
Spockのテストケースで以下のように「genPairwiseTestData」を使用してデータを生成します。genPairwiseTestDataはJCUnitの機能を呼び出すためのラッパーとして機能するstaticメソッドで定義は後述のテストコード全体に含まれています。
@Unroll
def "分配法則(a=#a,b=#b,c=#c)"() {
expect:
c*(a+b) == c*a + c*b
where:
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c } )
}
上記中、「[a,b,c] <<は」Spockのデータパイプという機能で、a,b,cというテストコードで使用する変数に対して、3要素のリストの集合を流し込むことで、パラメータのバリエーションに対してテストコードが複数回呼び出されるというものです。@Unrollはさらにそれをテストレポート上で複数のテストメソッドがあるかのように表示するアノテーションです。これらはいずれもSpockの機能に乗っとっていて、データパイプに流し込むデータをJCUnitの機能を使って生成することで、機能連携しています。
@FactorFieldはJCUnitが提供するアノテーションで、JCUnitで指定できる機能を利用できます*1。
上記では、[a,b,c]がすべてintの場合ですが、型がもし違っていれば、
[a,b,c] << genPairwiseTestData(new Object(){
@FactorField public int a
@FactorField public String a
@FactorField public double c
} )
のようにそれぞれ指定します。<<の左辺に表われる変数(上記ではa,b,c)は、右辺の@FactorFieldで同じ順序で一回ずつ指定される必要があります(冗長だが、現状の仕組みでは仕方がない)。
長いテストコード例
以下は単独で実行できるように@Grabで指定したSpockテストコードです。Grails中のSpockテストコードやGradleからの使用では適切な依存関係の指定で置き換えることになるでしょう。
groovy JCUnit.groovy
で実行できます。
@Grab('com.github.dakusui:jcunit:0.4.10')
@Grab('org.spockframework:spock-core:0.7-groovy-2.0')
import com.github.dakusui.jcunit.core.FactorField;
import com.github.dakusui.jcunit.generators.TupleGeneratorFactory;
import com.github.dakusui.jcunit.core.tuples.Tuple;
import com.github.dakusui.jcunit.generators.ipo2.IPO2;
import com.github.dakusui.jcunit.generators.ipo2.optimizers.GreedyIPO2Optimizer;
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(List<String> names, Closure c) {
return new ConstraintManagerBase() {
@Override
boolean check(Tuple tuple) throws UndefinedSymbol {
Map map = [:]
for (name in names) {
if (!tuple.containsKey(name)) {
throw new UndefinedSymbol();
}
map[name] = tuple.get(name)
}
c.delegate = map
c.call()
}
}
}
static Collection genPairwiseTestData(Object object, Closure constraint = null) {
def tg = TupleGeneratorFactory.INSTANCE.createTupleGeneratorForClass(object.class)
def names = tg.factors.collect{it.name}
def cm
if (constraint == null) {
cm = tg.constraintManager
}
else {
cm = closureConstraintManager(names, constraint)
}
IPO2 ipo2 = new IPO2(tg.factors, 2, cm, new GreedyIPO2Optimizer())
ipo2.ipo()
return ipo2.result.collect{ testData -> names.collect{ testData[it] } }
}
@Unroll
def "分配法則(a=#a,b=#b,c=#c)"() {
expect:
c*(a+b) == c*a + c*b
where:
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c } )
}
@Unroll
def test1() {
expect:
c*(a+b) == c*a + c*b
where:
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c }, { a > 0 && b != c })
}
@Unroll
def test2() {
expect:
(a+b).size() == a.size() + b.size()
where:
[a,b] << genPairwiseTestData(new Object(){ @FactorField public String a,b })
}
@Unroll
def test3() {
expect:
a+b == b+a
where:
[a,b] << genPairwiseTestData(new Object(){
@FactorField(intLevels=[1,2,3]) public int a
@FactorField public int b
})
}
@Unroll
def test4() {
expect:
a == b || a != b
where:
[a,b] << genPairwiseTestData(new Object(){
@FactorField public MyBoolean a
@FactorField public MyBoolean b
})
}
}
enum MyBoolean {
True,
False
}
データの生成のされかた
3つのintデータは、デフォルトでは以下のデータがペアワイズ法で生成されます。1, 0, -1, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUEの7種類の値が、a,b,cのパラメータに流し込まれるので、全組み合せだと7^3=343ケースが実行されるところを、53テストケースに絞り込まれていますね。
[[a:1, b:1, c:0],
[a:1, b:0, c:100],
[a:1, b:-1, c:-1],
[a:1, b:100, c:-100],
[a:1, b:-100, c:2147483647],
[a:1, b:2147483647, c:-2147483648],
[a:1, b:-2147483648, c:1],
[a:0, b:1, c:100],
[a:0, b:0, c:2147483647],
[a:0, b:-1, c:-100],
[a:0, b:100, c:0],
[a:0, b:-100, c:1],
[a:0, b:2147483647, c:-1],
[a:0, b:-2147483648, c:-2147483648],
[a:-1, b:1, c:2147483647],
[a:-1, b:0, c:-2147483648],
[a:-1, b:-1, c:1],
[a:-1, b:100, c:-1],
[a:-1, b:-100, c:100],
[a:-1, b:2147483647, c:-100],
[a:-1, b:-2147483648, c:0],
[a:100, b:1, c:-2147483648],
[a:100, b:0, c:-1],
[a:100, b:-1, c:0],
[a:100, b:100, c:1],
[a:100, b:-100, c:-100],
[a:100, b:2147483647, c:2147483647],
[a:100, b:-2147483648, c:100],
[a:-100, b:1, c:1],
[a:-100, b:0, c:0],
[a:-100, b:-1, c:-2147483648],
[a:-100, b:100, c:100],
[a:-100, b:-100, c:-1],
[a:-100, b:2147483647, c:100],
[a:-100, b:-2147483648, c:-100],
[a:2147483647, b:1, c:-100],
[a:2147483647, b:0, c:1],
[a:2147483647, b:-1, c:100],
[a:2147483647, b:100, c:2147483647],
[a:2147483647, b:-100, c:0],
[a:2147483647, b:2147483647, c:-2147483648],
[a:2147483647, b:-2147483648, c:-1],
[a:-2147483648, b:1, c:-1],
[a:-2147483648, b:0, c:-100],
[a:-2147483648, b:-1, c:2147483647],
[a:-2147483648, b:100, c:-2147483648],
[a:-2147483648, b:-100, c:-2147483648],
[a:-2147483648, b:2147483647, c:1],
[a:-2147483648, b:-2147483648, c:2147483647],
[a:-100, b:2147483647, c:2147483647],
[a:-2147483648, b:1, c:0],
[a:-2147483648, b:-100, c:100],
[a:1, b:2147483647, c:0]]
他のデータ型について、デフォルトではどのようなデータの集合(レベル)をつかってデータが生成されるかはこちら。
Enumは勝手に全要素を組合せてくれます。