ペアワイズ法でSpockのテストデータを生成する
(関連記事:http://uehaj.hatenablog.com/entry/2015/10/18/155107 )
JCUnitというすばらしいJUnitの拡張があります。
JCUnitではペアワイズ法もしくはオールペア法という手法でテストデータを生成します。ペアワイズ法というのは、試験対象コードに対するパラメタのバリエーションテストをする際に、より少ないテストデータの件数で、効率良くバグを発見できるというテストデータの選択技法だそうです。(オールペア法の記事)
折角の自動テストならば、テストデータも自動生成してしまおうと。しかし単純に総当たりだと、指数的にケース数が増えて手に負えなくなるので、統計学に基づいて、優れているとされている方法でデータを選択し実用的には十分なようにしよう、ということです。
JCUnitはJUnit4のRunner(@RunWithアノテーション)を使って実現されていますが、本記事では、そのエンジンだけを呼び出すことで、JCUnitをSpockのデータドリブンテストで使う方法を紹介します。
方法
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は勝手に全要素を組合せてくれます。
データ集合の指定
元になるデータの集合(レベル)を指定することもできます。たとえば、intについて1,2,3の3通りのデータからの組合せを生成するには、以下のようにします。
@FactorField(intLevels=[1,2,3]) public int a
制約
データが満たすべき制約條件を与えて、テストケース数をふるいにかけて減らすこともできます。
以下では、変数間の関係がa > 0 && b != cを満たすテストケースに絞り込みます。
[a,b,c] << genPairwiseTestData(new Object(){ @FactorField public int a,b,c }, { a > 0 && b != c })
Groovyなので、クロージャで制約式を与えられるようにしてみました。
おまけ
生成されるテストデータは毎回かわるわけではないので、テストデータをファイルにキャッシュする仕組みがあると良い気もする。
まとめ
JCUnitすばらしい!Spock便利! Groovy超便利!!
追記(2014/10/27)
JCUnitの更新情報として作者の方からコメントを頂いています。記事中では使用していませんが、'levelsFactory'メソッドが0.4.11か0.4.12で'levelsProvider'に改名されていますので注意。
SpockにおけるMock、Stub、Spyの違い
テスティングフレームワークSpockで相互作用中心のテストをする場合、いわゆるテストダブルとして、MockとStubとSpyの3種類を使えます。それぞれの意味などを簡単に解説します。
Spock全体含め詳しい解説はこちらなどを参照ください。
Mockに関しては、id:yamkazu 氏によるこちらのJGGUG G*Workshopの資料も秀逸です。
Stubとは
最初に、Stub(GroovyStub)についてです。Stubは後述Mockの低機能版であり、具体的には「モッキングができないMock」がStubです。StubができることはMockでできるので、本来Stubを使う必要はありません。モッキングしないことを明示したいなら、使えば良いでしょう。なお、Spock用語の「スタビング」とは別のものです(スタビングおよびモッキングについては後述)。
Mockとは
Mock(もしくはGroovyMock)は、テスト対象が使うクラスを指定して、そのクラスのインスタンスの真似をするオブジェクト(モックオブジェクト)を作ります。真似をするというのは、「元のクラスにあるのと同じシグネチャのメソッドを、モックオブジェクトに対して呼び出したりできる」ということです。Mockのメソッドを呼んでも、Mock元になったオブジェクトの本物のメソッドは呼び出されません。
たとえると、モックは「代役」です。本人は楽屋かなにかで休んでおり、リハーサル(テスト)では代役が呼び出しに応じて努めます。
以下、Mockの使用の例です:
class TestUnit3Spec extends UnitSpec { def test00() { setup: def bean = Mock(SomeBean3) when: def result = bean.methodOfSomeBean() println result then: 1 * bean.methodOfSomeBean() >> 123 result == 123 } } class SomeBean3 { int methodOfSomeBean() { println "hoge" 100 } }
上の例では、実際のメソッド methodOfSomeBean では「100」を返しているのにもかかわらず、あるいは"hoge"を出力しているのにもかかわらず、帰ってくる値は「123」だし(assert result=123)、println "hoge"は実行されず、当然"hoge"も出力されません。
ではその代役(モック)は何をするのか?というとですね、モックは、自身のメソッドが呼び出されたときに、「どんなメソッドがどんな引数で呼び出されようとしたか」を記録しており、期待するものだったかどうかを後でチェックできるようになっています。以下がそのチェック部分です.
1 * bean.methodOfSomeBean() >> 123
この場合、methodOfSomeBeanが1回、呼ばれるならチェックが成功します。
利点としては、上の場合SomeBean3のメソッドが実装されているかどうか、どのように実装されているか、正しいかバグがあるかと関係なく、シグネチャさえ決まっていれば「SomeBean3を使うコード」の試験ができることです。本来「単体試験」というのはかくあるべきだと思いますね。
注意
上の例では、SomeBean3をMockしてますが、「試験対象クラス」がSomeBean3だという想定ではありません。その意味で、上は、試験対象を試験してないテストという変な例です。「試験対象クラスが使用するクラス」に代役を立てることができるのであって、試験対象クラスそのものに代役をたてたら意味ありません。コラボレータとよばれる、「試験対象クラスの動作に必要なクラス」、脇役とか背景の群集とかに、代役を立てるってことです。
Mockまとめ:
- Mockは代役であり、本人の代りを務める。
- なので本番オブジェクトのメソッドができあがってなくても、それを使うオブジェクトの試験ができる。
- Mockに行なわれたメソッド呼び出しの有無と回数、引数などを記録をしてくれる機能ももっている
- それを後で確認できる。試験がはかどるわ〜。
Spyについて
これに対して、Spy(もしくはGroovySpy)は、その対象とするオブジェクトが、実際に存在する必要があります。偽物だけではなく、本物インスタンスが存在していることが必要です。
Spyの役割りは、「横からメソッド呼び出しの様子を監視し、その呼び出しメソッドや引数を記録するというものです。加えて、最後にその記録された情報、どんなメソッドがどんな回数呼びだされたかをチェックすることができます。Mockと同じようですが、Spyでは戻り値の「本当の値」でのチェックができます。反面、試験対象オブジェクト以外のコラボレータの振舞いに結果が左右され得ることになるので、単体試験としてはどうなんでしょう、という感じもします。
たとえると、スパイは「善意のマンインザミドル攻撃」のようなものです。呼び出し側と対象オブジェクトの間に挟まった代理人で、あとから整合性を確認するための記録を取ってくれます。
なお、Spyでは、スタビング指定すると、実際の値以外の値を返させ、元メソッドを呼ばない動作をさせることもできます。(メソッド個別にMockのように動作させることもできる)。
Spyまとめ:
- Spyは、本人とのやりとりを横から監視してやりとり記録する
- なので本番オブジェクトは必要。本番オブジェクトのメソッドの呼び出しの実際の実行、およびその返り値を使っての試験ができる。
- 本番オブジェクトのメソッドを呼ばずに、特定の別の値を返すこともできる(スタビング指定する)。
- 本番オブジェクトのメソッドを呼び、かつ、特定の別の値を返すこともできる(スタビングでクロージャ指定, callRealMethod使用)。
- なので本番オブジェクトは必要。本番オブジェクトのメソッドの呼び出しの実際の実行、およびその返り値を使っての試験ができる。
- 自身に行なわれたメソッド呼び出しの有無と回数、引数などを記録をしてくれる機能ももっている
- それを後で確認できる。試験がはかどるわ〜。
MockとSpyの違いと使い分け
MockとSpyの実用上の最も大きな違いは、Mockの場合はメソッドの返り値がテキトーなもの、0とかnullとかになるということです。実体がないからしょうがありませんね。試験の都合のために、Mockでそれらしい特定の値が返ってほしいときは>>で返り値を明示指定します(スタビング)。
Spyは実際の値がかえるから、>>をする必要はありません。あえて>>しても良いですけれども。
どちらをどのようなときに使い分ければ良いか、という指針ですが、SpockのドキュメントにはSpyについて「(この機能を使用する前に一度考えなおしてください。もしかすると仕様対象のコード設計自体を見なおしたほうが良いかもしれません。)」とのことです。なので、
「なるべくMockで、それでなんらかの不都合があるならSpyで」
でしょうか。
なお、SpockのMock,Spy,Stubという機能は、世に一般的に言う、モック、スパイ、スタブの説明とは微妙な違いがあるかもしれません。上はあくまでSpockのそれらの説明です。
おまけ: モッキングとスタビング
モッキング
Spockにおいて、モッキングとは、テスト対象クラスに対するMock, Spy, Stubのいずれかで、発生を期待するイベントすなわち「どんな引数でのメソッド呼び出しが何回、どんな順序で発生するか」の期待を記述しておくことを言います。
ジョセフ・ジョースターで言えば、「おまえは次に〜する!」「おまえは次に〜言う!」と予告のことです。テストが成功したら、「ハッやってしまったぁ〜!!」と言わねばなりません。
こういう感じになります(以下、こちらより引用)。(*)のところで「1 * ...」しているところがモッキング。
次の例を見てください。def "should send messages to all subscribers"() { when: publisher.send("hello") then: 1 * subscriber.receive("hello") // (*) 1 * subscriber2.receive("hello") // (*) }文章としてコードを読んでみると「publisher が ‘hello’ のメッセージを send したとき、 それぞれの subscriber は 1回 message を receive すべき」になります。
スタビング
スタビングは、メソッド呼び出しの返り値を(上書き)指定します。
こちらからまた引用しますが、以下のコード例の (**)のところで>>指定しているのがスタビングです。
固定の値を返す
すでにここまでの例の中で出てきましたが、固定の値を返すには算術右シフト(>>)演算子を使用します。subscriber.receive(_) >> "ok"呼び出し毎に異なる値を返すには、それぞれ個別のインタラクションとして定義します。
subscriber.receive("message1") >> "ok" // (**) subscriber.receive("message2") >> "fail" // (**)これは"message1"を受信すると"ok"を返し、"message2"を受信すると"fail" を返します。返す値に制限はありませんが、メソッドの戻り値型の制約を外れることはできません。
スタビングとモッキングは同時に行うことも可能です。
1 * subscriber.receive("message1") >> "ok"
モッキングはMockに、スタビングはStubに、という対応はありません。
ただし、Stubはモッキングができず、スタビングのみが行えます。
つまり以下のとおりです。
スタビング | モッキング | |
---|---|---|
Mock | ○ | ○ |
Spy | ○*1 | ○ |
Stub | ○ | × |
*1:固定値を指定してSpyをスタビングすると元メソッドの呼び出しは抑制される。
G*ワークショップZ May 2013 - Spockハンズオン5/17
G*ワークショップZ Spockハンズオンのおしらせ。
Spockを試してみたいかたは是非どうぞ。
- http://jggug.doorkeeper.jp/events/3872:title=http://jggug.doorkeeper.jp/events/3872
- 日時 2013/05/17 (金) 19:00 - 21:00
- 開催場所 NTTソフトウェア品川本社 品川グランドセントラルタワー17階 セミナールーム
NTTソフトウェアはオフィスが移転したので、今回から場所がいつもと違います。品川の同じ並びの別のビルです。
BDDフレームワークspockのドキュメントの翻訳
山田さんがBDDフレームワークであるspockのドキュメントを翻訳されておられます。
これは実に良い情報。
こちらをどうぞ。
Spock Framework リファレンスドキュメント — Spock 1.0-SNAPSHOT