Spock Framework リファレンスドキュメント¶
著者: Peter Niederwieser
Version: 1.0-SNAPSHOT
注釈
このドキュメントは現在作成中の状態です。当面の間は http://wiki.spockframework.org にある過去のドキュメントも合わせて参照してください。
目次¶
イントロダクション¶
SpockはJava、Groovy向けの、テストまたは仕様フレームワークです。他のツールと比べて何より特徴的なのは、その美しく表現力の高い仕様記述言語です。JUnitランナーの仕組みのお陰で、SpockはほとんどのIDEや、ビルドツール、継続的インテグレーションサーバと互換性があります。SpockはJUnit、JMock、RSpec、Groovy、Scala、Vulcans、その他な魅力的なツールからインスパイアされました。
Getting Started¶
Spockは非常に簡単に使い始めることができます。このセクションでは、それ例をいくつかご紹介します。
Spock Web コンソール¶
Spock Web Console はSpockのスペックを表示、編集、実行したり、さらに公開できるウェブサイトです。これは、ちょっとしたSpockのスペックを色々と試してみるのに最適の場所です。今すぐ Hello, Spock! を実行してみましょう!
Spock Example プロジェクト¶
自分のマシンでSpockを試してみるには、Example Project(download link)をダウンロードして、解凍してください。これはAnt、Gradle、またはAntでビルドできるようになっています。Gradleでのビルドは自身の起動に必要なファイルを自動で取得し、ワンコマンドでEclipse、またはIDEAでのビルド環境を構築することもできます。詳しくはREADMEを参照してください。
データ駆動テスト¶
よくある状況で、入力値と結果のバリエーションを検証するために、同じテストコードを複数回実行したいことがあります。Spockでは、このような場合に備えて、データ駆動テストの素晴らしい機能をサポートしています。
イントロダクション¶
まずMath.max
メソッドの振る舞いを定義したいとしましょう。
class MathSpec extends Specification {
def "maximum of two numbers"() {
expect:
// exercise math method for a few different inputs
Math.max(1, 3) == 3
Math.max(7, 4) == 7
Math.max(0, 0) == 0
}
}
このやり方は、簡単なケースでは非常によい方法ですが、いくつかの欠点もあります。
コードとデータが混在しており、簡単にどちらかを変更できない
データを簡単に自動生成したり、外部のリソースを読み込んだりできない
同じコードを複数回実行したい場合は、コードを複製するかコードの一部を別のメソッドに抽出する必要がある
実行が失敗した場合、失敗した時の入力値がすぐに分からない
同じコードを複数回実行する場合は、実行をしているコードを別のメソッドに切り出さない限り、分離したメリットを得られない
Spockのデータ駆動テストのサポートはこの問題を解決します。はじめに上記のコードを、データ駆動テストの機能を利用した方法に、リファクタリングしてみましょう。まず、ハードコーディングされた3つのinteger値をメソッドの引数(データ変数と呼びます)に置き換えます。
class MathSpec extends Specification {
def "maximum of two numbers"(int a, int b, int c) {
expect:
Math.max(a, b) == c
...
}
}
テスト自体の実装はこれで完了ですが、これとは別に入力値となるデータの定義が必要です。これはメソッドの最後に記述するwhere:
ブロックで定義します。もっとも単純(そして最も一般的)なやり方は、where:
ブロックでデータテーブルを使用する方法です。
データテーブル¶
データテーブルはフィーチャーメソッドに対し、固定のデータを定義する場合に便利な仕組みです。
class Math extends Specification {
def "maximum of two numbers"(int a, int b, int c) {
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 3 | 3
7 | 4 | 4
0 | 0 | 0
}
}
テーブルの1行目はテーブルヘッダと呼ばれ、変数名を定義します。この後に続く行はデータ行と呼ばれ、ヘッダに対応する値を保持します。データ行は、行毎にそれぞれ個別のフィーチャーメソッドとして実行されます。これをメソッドのイテレーションと呼んでいます。もしイテレーションの途中で実行が失敗した場合は、そこで停止せずに、後続のイテレーションが実行されます。最後に、途中で失敗したイテレーションは、すべてレポートされます。
データテーブルは最低でも2つの列を定義する必要があります。もし1つしか列がないテーブルを定義したい場合は、以下のようにします。
where:
a | _
1 | _
7 | _
0 | _
イテレーション内での実行の分離¶
イテレーションはそれぞれ別々のフィーチャーメソッドとして実行されます。各イテレーションはスペッククラス自身のインスタンスを取得し、setup
、cleanup
メソッドを、それぞれのイテレーションの実行前後に呼び出します。
イテレーション間のオブジェクトの共有¶
イテレーション間でオブジェクトを共有するには、@Share
またはstaticフィールドで値を保持してください。
注釈
where:
ブロックからは@Share
とstaticフィールド値へのアクセスのみが許可されています。
このような@Share
やstaticなオブジェクトは、他のメソッドへも共有されることに注意してください。特定のフィーチャーメソッド内に閉じて、イテレーション間でオブジェクトを共有する方法は、現在のところありません。もし、この問題をどうしても解決した場合は、ファイルを分けて、それぞれのフィーチャーメソッドを、別々のスペックファイルとすることを検討してください。だたし、これはファイルを分割することにより、実行に必要なコードを重複して記述しなければならない可能性があり、若干のコストが伴います。
シンタックスのバリエーション¶
さきほどのコードは、さらにいくつかの改善が可能です。まずはじめに where:
ブロックで定義したデータ変数の、メソッドのパラメータを省略できます[1]。次に、入力値と出力の期待値を、視覚的に区別するために、論理和の記号(||
)で区切ることができます。これを反映すると、コードは次のようになります。
class DataDriven extends Specification {
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
3 | 5 || 5
7 | 0 || 7
0 | 0 || 0
}
}
失敗のレポート¶
max
メソッドの実装に誤りがあり、イテレーションの途中で失敗したとしましょう。
maximum of two numbers FAILED
Condition not satisfied:
Math.max(a, b) == c
| | | | |
| 7 0 | 7
42 false
明白な質問: 何回目のイテレーションで失敗して、使用したデータは何でしょうか?この例では、2回目のイテレーションで失敗したことを把握するのは難しくありませんが、これが非常に困難、または不可能である場合もあります[2]。このような状況では、単に失敗だけをレポートするのではなく、どのイテレーションが成功したのか、または失敗したのかがわかると、非常にレポートがわかりやすくなります。これが@Unroll
アノテーションの目的です。
メソッドのUnroll¶
@Unroll
が付与されたメソッドは、イテレーションをそれぞれ独立したレポートとして出力します。
@Unroll
def "maximum of two numbers"() { ... }
@Unroll
はメソッドの実行結果には影響を与えず、違いがあるのはレポート結果だけです。実行環境によりますが、出力は次のようなものになります。
maximum of two numbers[0] PASSED
maximum of two numbers[1] FAILED
Math.max(a, b) == c
| | | | |
| 7 0 | 7
42 false
maximum of two numbers[2] PASSED
これは2回目のイテレーション(インデックスは1)が失敗したことを表しています。されにこれに手を加えることで、より見やすくできます。
@Unroll
def "maximum of #a and #b is #c"() { ... }
このように、メソッド名で変数の先頭にハッシュ(#
)を付与すると、プレースホルダとして使用できます。この例では、データ変数のa
、b
、そしてc
を参照するプレースホルダになります。このプレースホルダの出力は、以下のように実際に使用した値に置き換えられます。
maximum of 3 and 5 is 5 PASSED
maximum of 7 and 0 is 7 FAILED
Math.max(a, b) == c
| | | | |
| 7 0 | 7
42 false
maximum of 0 and 0 is 0 PASSED
このようにすることで、max
メソッドが入力値7
と0
で失敗したことが一目瞭然になります。このトピックの詳細はMore on Unrolled Method Namesを参照してください。
また、@Unroll
アノテーションはスペッククラスにも付与するすることできます。これはデータ駆動テストを行うフィーチャーメソッドそれぞれに、アノテーションを付与したのと同じ効果が得られます。
データパイプ¶
データテーブルだけが、データ変数を定義する唯一の方法というわけではありません。データテーブルは、実際には1つ、または複数のデータパイプのシンタックスシュガーです。
...
where:
a << [3, 7, 0]
b << [5, 0, 0]
c << [5, 7, 0]
データパイプは左シフト(<<
)演算子を使用し、データ変数にデータプロバイダを設定します。データプロバイダはすべての値を保持し、イテレーション毎に1つの値を使用します。データプロバイダには、Groovyでイテレーション可能なオブジェクトであれば、何でも使用することができます。これにはCollection
、String
や、Iterable
インタフェースを実装したオブジェクトが含まれます。データプロバイダは必ずしも、それがデータ(例えばCollection
)である必要はありません。データプロバイダにはテキストファイルや、データベース、スプレッドシート、またはランダムにデータを生成するといった、外部リソースからデータを取得することもできます。また、データプロバイダは値が必要になった時点(次のイテレーション前)で、はじめて次の値を取得します。
データパイプで複数の値を扱う¶
もしデータプロバイダ(Groovyがイテレーション方法を知っているオブジェクト)がイテレーション毎に複数の値を返す場合は、複数のデータ変数に対し、同時に値を設定することができます。シンタックスはGroovyのマルチ代入に似ていますが、左辺でパーレン(丸括弧)の代わりにブラケット(大括弧)を使用します。
@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
def "maximum of two numbers"() {
...
where:
[a, b, c] << sql.rows("select a, b, c from maxdata")
}
もし、使用しないデータがある場合は、アンダースコア(_
)で無視することができます。
...
where:
[a, b, _, c] << sql.rows("select * from maxdata")
データ変数への代入¶
データ変数へ直接代入することもできます。
...
where:
a = 3
b = Math.random() * 100
c = a > b ? a : b
データ変数への代入はイテレーション毎に再評価されます。上記の様に、代入の際に他のデータ変数を参照することもできます。
...
where:
row << sql.rows("select * from maxdata")
// pick apart columns
a = row.a
b = row.b
c = row.c
データテーブル、データパイプ、代入の組み合わせ¶
必要に応じてデータテーブル、データパイプ、代入を組み合わせて使用できます。
...
where:
a | _
3 | _
7 | _
0 | _
b << [5, 0, 0]
c = a > b ? a : b
イテレーションの回数¶
イテレーションの回数は、使用可能なデータがいくつあるかに依存しています。メソッドの繰り返し実行時に、各データプロバイダで異なるイテレーションの回数になる場合があります。このように、特定のデータプロバイダが、他のデータプロバイダよりも早く値が不足した場合は、例外がスローされます。ただし、代入はイテレーションの回数に影響を与えません。また、where:
ブロックが代入だけの場合は、1回だけイテレーションが実行されます。
データプロバイダのクローズ¶
全てのイテレーションが完了した後に、データプロバイダが引数なしのclose
メソッドを持っている場合は、自動的にそのメソッドが呼び出されます。
Unrollメソッド名の詳細¶
Unrollメソッド名はGroovyのGString
に似ていますが、以下の点が異なります。
式は
$
[3]の代わりに#
を使用し${...}
に相当するシンタックスはありません式はプロパティへのアクセスと引数なしのメソッド呼び出しのみがサポートされています
name
とage
というプロパティを持つPerson
クラスがあり、このPerson
の型がperson
というデータ変数として参照可能な場合、以下のように使用できます。
def "#person is #person.age years old"() { ... } // property access
def "#person.name.toUpperCase()"() { ... } // zero-arg method call
非stringの値(例えば上記の#person
)は、Groovyの挙動に従いStringに変換されます。
次のメソッド名は正しくありません。
def "#person.name.split(' ')[1]" { ... } // cannot have method arguments
def "#person.age / 2" { ... } // cannot use operators
必要に応じて、より複雑な式を保持するために、データ変数を活用することもできます。
def "#lastName"() {
...
where:
person << ...
lastName = person.name.split(' ')[1]
}
注記
[1] | メソッドの引数として宣言する理由としては、よりIDEのサポートが得られる点です。しかし、最近のIntellij IDEAではデータ変数を自動的に認識し、さらにはデータテーブルに含まれている値からその型を推論します。 |
[2] | 例えば、フィーチャーメソッドはデータ変数を |
[3] | Groovyのシンタックスはメソッド名にドル記号を使用できません。 |
相互作用中心のテスト¶
相互作用中心のテストは2000年代前半に登場したExtreme Programming(XP)から生まれた設計、テストの手法です。オブジェクトの状態ではなく振る舞いに注目し、メソッドを呼び出した際にコラボレータとオブジェクトの間で、意図したインタラクション(相互作用)が行われるか検証します。
例えば複数のSubscriber
にメッセージを送信するPublisher
があるとします。
class Publisher {
List<Subscriber> subscribers
void send(String message)
}
interface Subscriber {
void receive(String message)
}
class PublisherSpec extends Specification {
Publisher publisher = new Publisher()
}
どのようにPublisher
をテストすればよいでしょうか?状態中心のテストではpublisherがsubscriberを追跡できているか検証します。しかし、ここで一番の関心事は、publisherが送ったメッセージがsubscriberで受信できるかです。これを検証するにはSubscriber
の特別な実装を用意し、publisherとsubscriberのやり取りを監視します。このような実装をモックオブジェクトと呼びます。
subscriberのモック実装を開発者自身で用意することもできますが、インタラクションが複雑なメソッドが増えてくると、非常に手間になってきます。そこでモックフレームワークの登場です。このフレームワークはコラボレータがどのようにインタラクションすべきか定義することで、実際にその通りに動作したか検証することができます。
Javaの世界では、JMock、EasyMock、Mockitoといったモックフレームワークが人気があり、非常に成熟しています。これらのモックフレームワークをSpcokで使用することもできますが、Spcokの言語仕様に合わせて、より自然に記述できるように、モックフレームワークをSpock自身で再構築することを決めました。この判断は、相互作用中心のテストを容易にするため、Groovyの機能を活用し、より読みやすく、より楽しく書けるようにしたいという思いからです。この章を読み終えた時に、その目標が達成されていると感じていただけると幸いです。
特に何も明示していない限り、すべての機能はJavaとGroovyで使用することができます。
モックオブジェクトの作成¶
モックオブジェクトはMockingApi.Mock()
メソッドで作成します[1]。2つのsubscriberモックを作成してみましょう。
def subscriber = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)
または、IDEのサポートが得られやすいように、よりJavaのシンタックスに近い形で記述することもできます。
Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()
この場合のモックの型は、左辺の変数の型から推論されます。
注釈
もしモックの型を左辺で指定した場合は、右辺で型を省略することができます(指定しても問題ありません)。
モックオブジェクトは文字通りその型の実装(クラスの場合はサブクラス)を作成します。別の言い方をすると、上記の例でsubscriberはis-a Subscriberです。これは、この型を期待する静的型付けのコード(Java)に、このオブジェクトを渡せることを意味しています。
モックオブジェクトのデフォルト動作¶
初期状態のモックは何の振る舞いもしません。このオブジェクトのメソッドを呼び出すことはできますが、戻り値の型に応じたデフォルト値を返すだけです(false
、0
、またはnull
)。ただしObject.equals
、Object.hashCode
、Object.toString
メソッドは例外です。モックオブジェクトはオブジェクト毎に一意のハッシュコードを持ち、自身との比較にのみ等しくなります。そしてObject.toString
の文字列表現では、モックした型が含まれます。これらのデフォルトの動作はスタビングによりオーバーライド可能です。詳細はスタビングで説明します。
モックオブジェクトを設定する¶
publisherとモックのsubscriberを作成した後に、publisherに作成したsubscriberを設定する必要があります。
class PublisherSpec extends Specification {
Publisher publisher = new Publisher()
Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()
def setup() {
publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
publisher.subscribers << subscriber2
}
}
これで2つのオブジェクト間で行われるインタラクションについて記述する準備が整いました。
モッキング(Mocking)¶
モッキングとは、オブジェクトとそのコラボレータ間(で必須)の、インタラクションを定義するということです。次の例を見てください。
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 すべき」になります。
フィーチャーメソッドが実行されると、then:
ブロックで宣言したインタラクションにマッチする、when:
ブロックでのモックオブジェクトへの呼び出しが全て記録されます。もし期待したインタラクションが満たされない場合はInteractionNotSatisfiedError
(のサブクラス)がスローされます。この検証は自動的に行われ、開発者自身で検証のコードを記述する必要はありません。
インタラクション¶
then:
ブロックの詳細を見ていきます。上記では2つのインタラクションを定義していますが、それぞれ多重度(cardinality)、対象制約(target constraint)、メソッド制約(method constraint)、そして引数制約(argument constraint)の4つの個別パートから成り立っています。
1 * subscriber.receive("hello")
| | | |
| | | argument constraint
| | method constraint
| target constraint
cardinality
多重度¶
多重度は期待するメソッドの呼び出し回数を表しています。これは固定の数値、または範囲で指定できます。
1 * subscriber.receive("hello") // exactly one call
0 * subscriber.receive("hello") // zero calls
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1.._) * subscriber.receive("hello") // at least one call
(_..3) * subscriber.receive("hello") // at most three calls
_ * subscriber.receive("hello") // any number of calls, including zero
// (rarely needed; see 'Strict Mocking')
対象制約¶
対象制約は呼び出しを期待するモックオブジェクトを表しています。
1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello") // a call to any mock object
メソッド制約¶
メソッド制約は呼び出しを期待するメソッドを表しています。
1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello") // a method whose name matches the given regular expression
// (here: method name starts with 'r' and ends in 'e')
呼び出しを期待するメソッドがゲッターの場合は、Groovyのプロパティ構文が使用できます。
1 * subscriber.status // same as: 1 * subscriber.getStatus()
セッターの場合は、通常のメソッドと同じように指定する必要があります。
1 * subscriber.setStatus("ok") // NOT: 1 * subscriber.status = "ok"
引数制約¶
引数制約は呼び出しを期待するメソッドの引数を表しています。
1 * subscriber.receive("hello") // an argument that is equal to the String "hello"
1 * subscriber.receive(!"hello") // an argument that is unequal to the String "hello"
1 * subscriber.receive() // the empty argument list (would never match in our example)
1 * subscriber.receive(_) // any single argument (including null)
1 * subscriber.receive(*_) // any argument list (including the empty argument list)
1 * subscriber.receive(!null) // any non-null argument
1 * subscriber.receive(_ as String) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 }) // an argument that satisfies the given predicate
// (here: message length is greater than 3)
複数の引数を持つメソッドに対しても問題なく使用できます。
1 * process.invoke("ls", "-a", _, !null, { ["abcdefghiklmnopqrstuwx1"].contains(it) })
また可変長引数のメソッドに対しても、同じようにインタラクションを定義できます。
interface VarArgSubscriber {
void receive(String... messages)
}
...
subscriber.receive("hello", "goodbye")
Spock Deep Dive: Groovy Varargs
Groovyではメソッドの最後のパラメータを、配列として宣言すると可変長引数として扱います。可変長引数のメソッドと同じように、このようなメソッドに対してもインタラクションのマッチングが行えます。
すべてのメソッド呼び出しに対するマッチング¶
“何にでも”一致するという指定が便利な場合もあります。
1 * subscriber._(*_) // any method on subscriber, with any argument list
1 * subscriber._ // shortcut for and preferred over the above
1 * _._ // any method call on any mock object
1 * _ // shortcut for and preferred over the above
注釈
(_.._) * _._(*_) >> _
は有効なインタラクションの定義ですが、これは良いスタイルでもなければ、便利な場面があるわけでもありません。
Strictモッキング¶
上記のすべてのメソッド呼び出しに対するマッチングはどのような時に役立つでしょうか?よい例がstrictモッキングです。このモックのスタイルは明示的に定義したインタラクション以外を許容しません。
when:
publisher.publish("hello")
then:
1 * subscriber.receive("hello") // demand one 'receive' call on `subscriber`
_ * auditing._ // allow any interaction with 'auditing'
0 * _ // don't allow any other interaction
このように0 *
をthen:
ブロック、またはメソッドの最後のインタラクションとして定義することで効果を発揮します。また_ *
(任意の呼び出し回数)は、auditingコンポーネントに対して任意の呼び出し回数を許容することを表しています。
注釈
_ *
はstrictモッキングを行う場合にのみ有効です。特にスタビングを行う場合は必要になることはありません。例えば_ * auditing.record(_) >> "ok"
は単にauditing.record(_) >> "ok"
と定義すべきです。
インタラクションの宣言場所¶
ここまで見てきた例ではインタラクションを、then:
ブロックに定義してきました。 これはより自然にスペックが読めるという効果がありますが、インタラクションをwhen:
ブロックの前で定義することもできます。これはsetup
といったメソッドでも定義できることを意味しています。同じように他のヘルパークラスでインタラクションを定義し、スペッククラス内から呼び出すといったことも可能です。
モックオブジェクトへの呼び出しが発生すると、インタラクションは定義した順番に評価されます。もし、呼び出しが複数のインタラクションにマッチする場合はマッチの上限に達してない限り、先に定義したインタラクションが優先して評価されます。ただし例外がありthen:
ブロックで定義したインタラクションは他のインタラクションよりも先に評価されます。これはsetup
など他のメソッドやブロックで定義したインタラクションよりもthen:
ブロックで定義したインタラクションが先に評価されることを意味しています。
Spock Deep Dive: How Are Interactions Recognized?
(質問: どのように通常のメソッドではなくインタラクションの定義だと識別しているのか?)Spockではインタラクションの定義を識別するために、簡単なルールを持っています。これは、式があった場合に、乗算(*
)、または左シフト(>>
, >>>
)のどちらか含まれていた場合に、インタラクションの定義として識別します。このような式は、本来もっている式の意味を変えることで、正しく動作してます。また、多重度の定義(モッキング)と、レスポンス定義(スタビング)のシンタックスを間違わないようにしてください。これらは必ずどちらかが、定義されなければなりません。foo.bar()
といった単独の式が、インタラクションとして識別されることはありません。
モック作成時のインタラクション定義(New in 0.7)¶
もしモックが動的に変化しない”基本”となるインタラクションがある場合は、モック作成時にインタラクションを定義できます。
def subscriber = Mock(Subscriber) {
1 * receive("hello")
1 * receive("goodbye")
}
この機能はスタビングにより専用のスタブを作る際にも便利な機能です。ここで、インタラクションに対象制約を含んでいない(参照できない[2])ことに注意してください。これは作成するモックのコンテキストで動作することが明白なためです。
また、フィールドをモックで初期化する場合にも使用できます。
class MySpec extends Specification {
Subscriber subscriber = Mock {
1 * receive("hello")
1 * receive("goodbye")
}
}
インタラクションのグループ化(New in 0.7)¶
Specification.with
ブロックで対象制約を共有することで、インタラクションのグループ化が行えます。これはモック作成時のインタラクション宣言と同じように、対象制約を繰り返し指定する必要がなくなります。
with(subscriber) {
1 * receive("hello")
1 * receive("goodbye")
}
また、with
ブロックは特定の対象に対する、コンディションのグループ化にも使用できます。
インタラクションとコンディションの組み合わせ¶
then:
ブロックにはインタラクションと、コンディションの両方を同時に含むことができます。厳密には必須ではありませんが、コンディションの前にインタラクションを定義するのが一般的です。
when:
publisher.send("hello")
then:
1 * subscriber.receive("hello")
publisher.messageCount == 1
文章としてコードを読んでみると「publisher が ‘hello’ のメッセージを send したとき、 subscriber は 1回 message を receive して、publisher の message count は 1 であるべき」になります。
明示的なインタラクションブロック¶
仕組み上、Spockは実行前に定義された、インタラクション全ての情報を保持する必要があります。では、なぜthen:
ブロックでインタラクションが定義可能なのでしょうか?内部的には、then:
ブロックで定義したインタラクションは、実行前にwhen:
ブロックの前に移動されます。これは多くの場合に問題なく動作しますが、特定の条件下では問題を引き起こす場合があります。
when:
publisher.send("hello")
then:
def message = "hello"
1 * subscriber.receive(message)
この例では期待する引数を変数に受けています(多重度を変数に受ける場合も場合も同様)。ここでインタラクションを移動すると、インタラクションからこの後で宣言する変数を参照していることになり、もちろんこのような参照は許容されません。このように単純にインタラクションを移動しただけでは、実行時にMissingPropertyException
を引き起こします。
この問題を解決する一つの方法は変数宣言を(少なくとも)when:
ブロックの前に移動することです(データ駆動テストが好きなユーザはwhere:
ブロックに変数を移動するかもしれません)。上の例では、副作用として送信するメッセージと同じ変数を共有できるというメリットもあります。
もう一つの解決方法は、変数をインタラクションと共に使用することを明示することです。
when:
publisher.send("hello")
then:
interaction {
def message = "hello"
1 * subscriber.receive(message)
}
MockingApi.interaction
ブロックは、常にそのブロック全体が移動するため、このコードは意図した通りに動作します。
インタラクションのスコープ¶
then:
ブロックで定義したインタラクションは、直前のwhen:
ブロックのみがスコープの対象となります。
when:
publisher.send("message1")
then:
subscriber.receive("message1")
when:
publisher.send("message2")
then:
subscriber.receive("message2")
これは最初のwhen:
ブロックでsubscriber
が"message1"
を受信した確認を行い、次のwhen:
で"message2"
を受信した確認を行なっています。
then:
ブロックの外で宣言したインタラクションは、そのフィーチャーメソッドが完了するまで有効になります。
また、インタラクションは常に特定のフィーチャーメソッドに対してスコープを持ちます。そのため、インタラクションをstaticメソッド、setupSpec
メソッド、またcleanupSpec
メソッドで定義することはできません。このような理由により、static、または@Shared
フィールドにもモックオブジェクトを設定すべきではありません。
インタラクションの検証¶
モックベースのテストが失敗する理由は主に2つです。期待したインタラクション以上に呼び出しが発生するか、もしくは期待したインタラクションに呼び出しが満たいない場合です。前者の場合は、TooManyInvocationsError
が発生し、実際に発生した呼び出し回数が表示されます。
Too many invocations for:
2 * subscriber.receive(_) (3 invocations)
このようなエラーが発生した場合は、期待した以上に呼び出しが発生した原因の解析が容易に行えるよう、呼び出しに関連する全てのインタラクションを表示します(new in Spock 0.7)。
Matching invocations (ordered by last occurrence):
2 * subscriber.receive("hello") <-- this triggered the error
1 * subscriber.receive("goodbye")
この出力によるとreceive("hello")
が原因となりTooManyInvocationsError
発生しています。ここではsubscriber.receive("hello")
の呼び出しでエラーがあったということに、情報が集約されてしまうことに注意してください。receive("hello")
で先にエラーとなるとreceive("goodbye")
にもエラーがあった場合と区別が付きません。
2つめの場合(期待したインタラクションに呼び出しが満たいない場合)は、when
ブロックの実行が完了した時点でエラーを検出します(終了するまでは呼び出しの可能性があるため)。エラーの場合はTooFewInvocationsError
が発生します。
Too few invocations for:
1 * subscriber.receive("hello") (0 invocations)
これはメソッドの呼び出しが全くなかったということではありません。同じメソッドが、他の引数で呼び出された、異なるモックオブジェクトで呼び出された、もしくは”別の”メソッドが呼び出されたといったことが考えられます。これらいずれの場合でも、TooFewInvocationsError
のエラーが発生します。
なぜ期待した呼び出しが行われなかったかを簡単に解析できるように、インタラクションに一致しない呼び出しがあった場合は、呼び出しに類似している順に、全てのインタラクションを表示します(new in Spock 0.7)。具体的には引数が一致するインタラクションを最初に表示します。
Unmatched invocations (ordered by similarity):
1 * subscriber.receive("goodbye")
1 * subscriber2.receive("hello")
実行順序¶
多くの場合にメソッドの呼び出し順序は重要ではありません。また実行順序は時間と共に変更される可能性が高いものでもあります。Spockはデフォルトでは、over-specificationを避けるため、最終的に定義したインタラクションすべてが満たされていれば、どのような実行順序でも許容します。
then:
2 * subscriber.receive("hello")
1 * subscriber.receive("goodbye")
ここで呼び出し順序は "hello"
"hello"
"goodbye"
、"hello"
"goodbye"
"hello"
、または "goodbye"
"hello"
"hello"
のいずれの場合も定義したインタラクションを満たしています。
実行順序が重要な意味をもつ場合は、インタラクションを複数のthen:
ブロックに分割することで、順序の検証が行えます。
then:
2 * subscriber.receive("hello")
then:
1 * subscriber.receive("goodbye")
これは"goodby"
を受信する前に、"hello"
を2回受信することを検証しています。このようにthen:
ブロック内では実行順序の検証を行いませんが、then:
ブロックを複数に分割することで、ブロック間の実行順序の検証が行えます。
注釈
then:
ブロックをand:
を使用して分割した場合は、順序の検証を行なっていることにはなりません。and:
は単なるドキュメンテーション目的のために存在しており、実行時の動作には何も影響を与えません。
クラスのモッキング¶
Spockではインタフェースの他に、クラスのモッキングもサポートしています。クラスのモッキングも、インターフェースと同じように使用できます。ただしクラスをモッキングする場合はcglib-nodep-2.2
以上、objenesis-1.2
以上が、クラスパスに含まれている必要があります。もしこれらのライブラリが、クラスパスに含まれていない場合はSpockが警告を出力します。
スタビング(Stubbing)¶
スタビングとは特定のメソッド呼び出しに対するレスポンスを定義することです。メソッドをスタビングする場合は、そのメソッドが何回呼ばれるかを気にせず、いつ呼び出されても特定の値を返したり、何かの副作用が働くようにします。
例を示すために、Subscriber
のreceive
メソッドの処理が完了した場合に、ステータスコードを返すように変更してみましょう。
interface Subscriber {
String receive(String message)
}
ここでreceive
メソッドが常に"ok"
を返すようにします。
subscriber.receive(_) >> "ok"
文章としてコードを読んでみると「subscriber が メッセージ を 受信 するたびに ‘ok’ を返す」になります。
モック時のインタラクションと比べると、スタブのインタラクションは左に多重度がない代わりに、右にレスポンスジェネレータを指定します。
subscriber.receive(_) >> "ok"
| | | |
| | | response generator
| | argument constraint
| method constraint
target constraint
スタブのインタラクションは、then:
ブロック内やwhen:
ブロックの前など、どのような場所でも宣言することができます(詳細はインタラクションの宣言場所を参照)。もし、モックオブジェクトをスタブとしてのみ使用する場合は、モック作成時やsetup:
ブロック内でインタラクションを定義するのが一般的です。
固定の値を返す¶
すでにここまでの例の中で出てきましたが、固定の値を返すには算術右シフト(>>
)演算子を使用します。
subscriber.receive(_) >> "ok"
呼び出し毎に異なる値を返すには、それぞれ個別のインタラクションとして定義します。
subscriber.receive("message1") >> "ok"
subscriber.receive("message2") >> "fail"
これは"message1"
を受信すると"ok"
を返し、"message2"
を受信すると"fail"
を返します。返す値に制限はありませんが、メソッドの戻り値型の制約を外れることはできません。
一連の値を返す¶
連続した呼び出しに対し異なる値を返すには、論理右シフト演算子(>>>
)を使用します。
subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
これは初めの呼び出しに対し"ok"
を返し、2回目3回目には"error"
、そして4回目以降は"ok"
を返します。右辺に指定する値はGroovyがイテレーション方法を知っている値である必要があります。この例では単なるリストを使用しています。
動的に値を返す¶
メソッドの引数に応じて動的に値を返すには、算術右シフト(>>
)演算子とクロージャを使用します。もしクロージャの引数が、型指定なしで1つの場合は、引数のリストが渡されます。
subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }
これはメッセージの長さが3文字以上の場合は"ok"
を返し、それ以外は"fail"
を返します。
しかしほとんどのケースでは、メソッドの引数に直接アクセスできたほうが便利でしょう。クロージャの引数を型付きで1つ以上定義した場合は、メソッドの引数がそれぞれクロージャの引数にマップされます。[4]
subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }
このモックは先程の例とまったく同じように動作しますが、より読みやすくなっています。
もし、メソッドの引数以上に実行中のメソッドについて、情報が必要な場合はorg.spockframework.mock.IMockInvocation
を参照してください。このインターフェース内の全てのメソッドは、クロージャ内からプレフィックス指定なしに呼ぶことができます。(Groovyの用語で言うとクロージャはIMockInvocation
のインスタンスへデリゲートします。)
副作用の実行¶
場合によっては動的に値を返す以外の処理が必要になることがあります。たとえば例外のスローです。このような場合にもクロージャを使用します。
subscriber.receive(_) >> { throw new InternalError("ouch") }
もちろんクロージャの中にはprintln
といった、さまざまなコードを含むことができます。この例ではメソッドの呼び出しが、インタラクションと一致するたびに例外をスローします。
メソッドのレスポンスをチェーンする¶
メソッドのレスポンスをチェーンすることができます。
subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"
これは最初の3回の呼び出しに対し"ok", "fail", "ok"
を返し、4回目にはInternalError
をスロー、それ以降の呼び出しにはok
を返します。
モッキングとスタビングの組み合わせ¶
モッキングとスタビングを組みわせて使用できます。
1 * subscriber.receive("message1") >> "ok"
1 * subscriber.receive("message2") >> "fail"
モッキングとスタビングを同じメソッドに対して行う場合は、同じインタラクションとして定義する必要があります。これは、Mockitoのようにモッキングとスタビングを、別々に定義することができないということです。
setup:
subscriber.receive("message1") >> "ok"
when:
publisher.send("message1")
then:
1 * subscriber.receive("message1")
インタラクションの宣言場所で説明したようにreceive
が呼ばれると、then:
ブロックで定義したインタラクションが始めに評価されます。このインタラクションは、特定のレスポンスを定義していないため、メソッドのデフォルトの値(この場合はnull)を返します(これはSpockがlenientなモックのアプローチを採用しているためです)。このため、setup:
ブロックで定義したスタビングのインタラクションは評価されることがありません。
注釈
同じメソッド呼び出しに対するモッキングとスタビングは、同じインタラクションとして定義する必要があります。
他のモックオブジェクト(New in 0.7)¶
ここまではMockingApi.Mock
メソッドを使用してモックを作成してきました。MockingApi
クラスは、これ以外にも特別なモックを作成するファクトリメソッドを提供しています。
スタブ¶
スタブはMockingApi.Stub
メソッドで作成します。
def subscriber = Stub(Subscriber)
モックはモッキング、スタビングの両方を行うことができましたが、スタブではスタビングのみが行えます。これはインタラクションの機能を制限することで、読み手に対し役割をより明確にできるメリットがあります。
注釈
もしスタブでインタラクションを定義した場合は(1 * foo.bar()
のように)、InvalidSpecException
がスローされます。
モックと同じように、スタブも予期していないメソッド呼び出しを許容しています。しかし、スタブはより実際の値に近い値を返す点が異なります。
プリミティブ型は、プリミティブ型のデフォルト値を返します。
非プリミティブ数値(
BigDecimal
のような)の場合は、ゼロを返します。数字以外の値は”空”や”ダミー”オブジェクトを返します。これは、空文字や、空のコレクション、デフォルトコンストラクトから生成したオブジェクト、またはデフォルトの値を持ったスタブオブジェクトを返すということ意味します。詳細は
org.spockframework.mock.EmptyOrDummyResponse
クラスを参照してください。
スタブはモック作成時のインタラクション宣言と同じように、インタラクションの固定セットを定義することができます。
def subscriber = Stub(Subscriber) {
receive("message1") >> "ok"
receive("message2") >> "fail"
}
スパイ¶
(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)
スパイはMockingApi.Spy
のファクトリメソッドで作成します。
def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])
スパイは常に本物のオブジェクトである必要があります。そのため、インタフェースではなくクラスの型をコンストラクタの引数に指定してください。もしコンストラクタの引数を指定しなかった場合は、デフォルトコンストラクタが使用されます。
スパイのメソッド呼び出しは、自動的に本物のオブジェクトに委譲されます。同様にメソッドの戻り値は、本物のオブジェクトからスパイを経由して呼び出し元に返ります。
スパイを作成すると、スパイを通して行われた呼び出し元と実際のオブジェクトとのやり取りを監視することができます。
1 * subscriber.receive(_)
このようにpublisherとSubscriberImpl
を変更することなく、スパイを使用してreceive
が1回呼ばれたことを確認できます。
またスパイでメソッドのスタビングを行うと、本物のメソッドが呼ばれなくなります。
subscriber.receive(_) >> "ok"
これは本物のSubscriberImpl.receive
が呼ばれる代わりに、receive
メソッドが単に"ok"
を返すようになります。
さらに、場合によっては、任意のコード実行と、本物のメソッドへの委譲の両方を組み合わせたい場合もでしょう。
subscriber.receive(_) >> { String message -> callRealMethod(); message.size() > 3 ? "ok" : "fail" }
このように本物のメソッドに処理を委譲する場合はcallRealMethod()
メソッドを使用します。ここで、呼び出す際にmessage
の引数を設定していないことに注意してください。このメソッドの引数は、Spockが自動的に設定します。この例ではcallRealMethod()
の呼び出しに本当の処理結果が返りますが、この値を使用せず値を差し替えています。また、呼び出しと異なる引数を、本物のメソッドに設定したい場合はcallRealMethodWithArgs("changed message")
を使用できます。
パーシャルモック¶
(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)
スパイはパーシャルモックとしても使用できます。
// this is now the object under specification, not a collaborator
def persister = Spy(MessagePersister) {
// stub a call on the same object
isPersistable(_) >> true
}
when:
persister.receive("msg")
then:
// demand a call on the same object
1 * persister.persist("msg")
Groovyモック(New in 0.7)¶
これまで説明してきたモックの機能は、呼び出し元のコードがJava、またはGroovyのどちらでも問題なく動作します。Groovyモックは、Groovyの動的な性質を活用したコードをテストするための、Groovy固有のテスト機能をサポートしています。これらはMockingApi.GroovyMock()
、MockingApi.GroovyStub()
、MockingApi.GroovySpy()
のファクトリメソッドを使用して作成できます。
When Should Groovy Mocks be Favored over Regular Mocks?
(質問: どのような場合に通常モックではなくGroovyモックを使用すべきか?)内部の実装にGroovyを使用していて、かつGroovy固有のモック機能が必要な場合に使用してください。もしJavaのコードからGroovyモックが呼び出された場合は、Groovyモックは通常のモックと同じように振舞います。ただしこのような場合は、そもそもGroovyモックを使用する必要がありません。Groovyモックは、Groovyで書かれたコードに対して、Groovy固有のテスト機能を追加しており、Javaから利用する際はこの機能が意味を持たないためです。もし、Groovyモックを使用する特別な理由がない限り、通常のモック機能を使用してください。
動的メソッドのモッキング¶
すべてのGroovyモックはGroovyObject
のインタフェースを実装しています。これらは通常のメソッドと同じように、動的メソッドへのモッキングとスタビングをサポートしています。
def subscriber = GroovyMock(Subscriber)
1 * subscriber.someDynamicMethod("hello")
任意の型すべてのインスタンスをモッキング¶
(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)
通常のモックと同様に、Groovyモックも実際のインスタンスの値をモックで差し替える必要があります。しかしGroovyモックをグローバルなモックとして作成した場合は、フィーチャーメソッドの実行の間、モックした型の全てのインスタンスを、自動的に差し替えることができます。[3]
def publisher = new Publisher()
publisher << new RealSubscriber() << new RealSubscriber()
def anySubscriber = GroovyMock(RealSubscriber, global: true)
when:
publisher.publish("message")
then:
2 * anySubscriber.receive("message")
はじめに2つの本物のsubscriber実装インスタンスを持つpublisherを準備します。次に、この本物のsubscriber実装の型を指定して、グローバルモックを作成します。このようにすることで本物のsubscriberに対する全てのメソッド呼び出しが、モックオブジェクトへ送られるようになります。また、ここでモックオブジェクトのインスタンスをpublisherに設定していません。このモックオブジェクトは、インタラクションを定義するためだけに使用します。
注釈
グローバルモックはクラスの型にのみ使用できます。これはフィーチャーメソッドが実行中の間、その型の全てのインスタンスが差し替わります。
グローバルモックは全てのインスタンスに影響を与えますが、グローバルに使用する場合はGroovySpy
も非常に便利です。これは、必要な部分の動作だけ変更を行い、インタラクションが一致しない場合は実際のメソッドを実行することが可能です。また、呼び出しの確認にも使用できます。
コンストラクタのモッキング¶
(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)
グローバルモックはコンストラクタのモッキングをサポートしています。
def anySubscriber = GroovySpy(RealSubscriber, global: true)
1 * new RealSubscriber("Fred")
ここではスパイを使用しているため、コンストラクタの振る舞いは変更されずに、モックオブジェクトが作成されます。コンストラクタの振る舞い変更するには、コンストラクタをスタビングします。
new RealSubscriber("Fred") >> new RealSubscriber("Barney")
このようにすることでFredという名前のsubscriberを構築するたびに、Barneyという名前のsubscriberが代わりに構築されます。
Staticメソッドのモッキング¶
(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)
グローバルモックはstaticメソッドの、モッキングとスタビングをサポートしています。
def anySubscriber = GroovySpy(RealSubscriber, global: true)
1 * RealSubscriber.someStaticMethod("hello") >> 42
これは動的なstaticメソッドに対しても使用できます。
もしグローバルモックをコンストラクタ、またはstaticメソッドのモックにのみ使用している場合は、モックインスタンスは必要がありません。このような場合は、以下のように書くこともできます。
GroovySpy(RealSubscriber, global: true)
高度な機能(New in 0.7)¶
ほとんどの人にとってはこの機能は必要がありません。しかし、一部のユーザにとっては非常に有益な機能なはずです。
アラカルトモック¶
これが最後の話題になります。Mock()
、Stub()
、そしてSpy()
のファクトリメソッドは、任意の構成でモックオブジェクトを作成するための方法が、あらかじめ用意されています。もしこの構成をより細かく制御したい場合は、まずorg.spockframework.mock.IMockConfiguration
インタフェースを参照してください。このインタフェース[5]の全てのプロパティは、Mock()
メソッドへ名前付き引数として設定することができます。例えば以下のように使用します。
def person = Mock(name: "Fred", type: Person, defaultResponse: ZeroOrNullResponse, verified: false)
ここで作成したモックは、通常のMock()
で作成した場合と同じデフォルト値を返しますが、(Stub()
のように)呼び出しの確認を行いません。また、ZeroOrNullResponse
を設定する代わりに、予期しないメソッドの呼び出しに対応するといった、独自のorg.spockframework.mock.IDefaultResponse
実装を設定することもできます。
モックオブジェクトの検出¶
オブジェクトがSpockのモックオブジェクトであるか調べるには、org.spockframework.mock.MockDetector
を使用します。
def detector = new MockDetector()
def list1 = []
def list2 = Mock(List)
expect:
!detector.isMock(list1)
detector.isMock(list2)
また、detectorはモックオブジェクトの詳細情報を取得することもできます。
def mock = detector.asMock(list2)
expect:
mock.name == "list2"
mock.type == List
mock.nature == MockNature.MOCK
参考文献¶
相互作用中心のテストについて、次の情報を参照することをおすすめします。
Endo-Testing: Unit Testing with Mock Objects
モックオブジェクトの概念を説明した、XP2000カンファレンスの資料
-
どのように適切にモッキングするか説明した、OOPSLA2004カンファレンスの資料
-
モックに関するMartin Fowlerの資料
Growing Object-Oriented Software Guided by Tests
TDDのパイオニアであるSteve FreemanとNat Pryceが、実際にどのようにモックを活用しながら、テスト駆動開発を行うのか詳細に説明している
注釈
[1] | モックオブジェクトを作成する他の方法については、他のモックオブジェクト(New in 0.7)とアラカルトモックを参照してください。 |
[2] | 同じステートメントの一部として宣言しているため、クロージャから |
[3] |
[4] | このクロージャの引数の代入はGroovyの動作によるものです。 |
[5] | モックの構成はイミュータブルであるため、インタフェースにはプロパティのゲッターのみが含まれています。 |
拡張機能¶
Spockでは、スペックのライフサイクルの振る舞いを、拡張したり、変更したりすることができる、強力な拡張機構を持っています。この章では、はじめにSpockにビルトインされている機能拡張について説明します。その後で、カスタムの機能拡張を記述する方法を説明します。
ビルトイン機能拡張¶
Spockでビルトインされている拡張機能のほとんどは、アノテーションを通して使用します。これはスペッククラスやメソッドに対して、特定のアノテーションを付与することで動作します。これらは@ExtensionAnnotation
のメタアノテーションを付与することで、独自に指定することも可能です。
Ignore¶
spock.lang.Ignore
を付与することで、一時的にフィーチャーメソッド実行しないようにできます。
@Ignore
def "my feature"() { ... }
実行しない理由を記述することもできます。
@Ignore(reason = "TODO")
def "my feature"() { ... }
全てのフィーチャーメソッドをignoreするには、クラスに対してアノテーションを付与します。
@Ignore
class MySpec extends Specification { ... }
ほとんどの実行環境では、ignoreされたフィーチャーメソッド、スペックは”skipped”としてレポートされます。
IgnoreRest¶
特定のメソッドのサブセット以外すべてをignoreするには、spock.lang.IgnoreRest
アノテーションを使用します。
def "I'll be ignored"() { ... }
@IgnoreRest
def "I'll run"() { ... }
def "I'll also be ignored"() { ... }
@IgnoreRest
は、特定のメソッドのサブセットを、(簡単に)実行する方法がない実行環境で、特に便利な機能です。
IgnoreIf¶
特定の条件下でのみフィーチャーメソッドをignoreするには、spock.lang.IgnoreIf
を付与し、プレディケートを指定します。
@IgnoreIf({ System.getProperty("os.name").contains("windows") })
def "I'll run everywhere but on Windows"() { ... }
プリディケートが読み書きしやすいように、クロージャ内で以下のプロパティが使用可能です。
sys
すべてのシステムプロパティのマップ
env
すべての環境変数のマップ
os
オペレーティングシステムに関する情報 (spock.util.environment.OperatingSystem
参照)
jvm
JVMに関する情報(spock.util.environment.Jvm
参照)
上記の例は、os
プロパティを使用することで以下のように記述できます。
@IgnoreIf({ os.windows })
def "I'll run everywhere but on Windows"() { ... }
Requires¶
特定の条件下でのみフィーチャーメソッドを実行するには、spock.lang.Requires
を付与し、プレディケートを指定します。
@Requires({ os.windows })
def "I'll only run on Windows"() { ... }
Requires
はプレディケートの意味が逆になっている以外はIgnoreIf
と同じように動作します。一般的には、ignoreする条件を記述するよりも、メソッドを実行する条件を明示する方が、良い作法とされています。
TODO More to follow.
カスタム機能拡張の作成¶
TODO
新機能と主な変更点¶
0.7¶
スナップショットリポジトリの移動¶
Spockのスナップショットは http://oss.sonatype.org/content/repositories/snapshots/ から取得できるようになりました。
新しいリファレンスドキュメント¶
Spockの新しいリファレンスドキュメントはhttp://docs.spockframework.orgで参照できます。ここにはhttp://wiki.spockframework.orgにあるドキュメントを徐々に移動していきます。このドキュメントは各バージョン毎に公開しています(例えばhttp://docs.spockframework.org/en/spock-0.7-groovy-1.8)。スナップショットの最新ドキュメントはhttp://docs.spockframework.org/en/latestを参照してください。Spock 0.7の時点ではデータ駆動テストと相互作用中心のテストの記述が完了しています。
TooManyInvocationsError
のモッキング失敗メッセージの改善¶
TooManyInvocationsError
発生時の診断メッセージが大幅に改善されました。以下に例を示します。
Too many invocations for:
3 * person.sing(_) (4 invocations)
Matching invocations (ordered by last occurrence):
2 * person.sing("do") <-- this triggered the error
1 * person.sing("re")
1 * person.sing("mi")
TooFewInvocationsError
のモッキング失敗メッセージの改善¶
TooFewInvocationsError
発生時の診断メッセージが大幅に改善されました。以下に例を示します。
Too few invocations for:
1 * person.sing("fa") (0 invocations)
Unmatched invocations (ordered by similarity):
1 * person.sing("re")
1 * person.say("fa")
1 * person2.shout("mi")
スタブ¶
従来のモックオブジェクトに加えて、スタブのサポートを追加しました。
def person = Stub(Person)
スタブはモックオブジェクトの機能制限版で、モックに比べより本物の値に近い値を返します。また、スタブは多重度を定義できない以外は、モックのインタラクションと同じよう見えますが、読み手に対し役割をより明確にできる効果があります。
スパイ¶
従来のモックオブジェクトに加えて、スパイのサポートを追加しました。
def person = Spy(Person, constructorArgs: ["Fred"])
スパイは、この例のPerson
インスタンスのように本物のオブジェクト上に構築されます。インタラクションが一致しないスパイに対するすべての呼び出しは、本物のオブジェクトへと処理が委譲されます。このようにスパイを使用することで、本物のオブジェクトの必要な部分のみ振る舞いを変更し、またその呼び出しを監視することができます。さらに、スパイはパーチャルモックとして使用することも可能です。
モック作成時のインタラクション定義¶
モック作成時にインタラクションを定義できようになりました。
def person = Mock(Person) {
sing() >> "tra-la-la
3 * eat()
}
この機能は、特にStubsで便利な機能です。
Groovyモック¶
Groovyのコードに特化した特別なモックオブジェクトの機能を追加しました。
def mock = GroovyMock(Person)
def stub = GroovyStub(Person)
def spy = GroovySpy(Person)
Groovyモックは、自動的にgroovy.lang.GroovyObject
を実装します。これは静的に定義されたメソッドと同じように、動的なメソッドに対するスタビングとモッキングが可能です。Groovyモックは、GroovyのコードではなくJavaから呼び出されると、通常のモックと同じように振舞います。
グローバルモック¶
Groovyモックはグローバルに作成できます。
GroovySpy(Person, global: true)
このグローバルモックはクラスの型にのみ使用できます。これは、その型の全てのインスタンスを差し替え、スタビングとモッキングはこれらの型全てに影響を与えます(このような動作をするGroovyのMockFor
とStubFor
をご存知かも知れません)。さらにグローバルモックは、その型のコンストラクタ、およびstaticメソッドのモッキングが可能です。
コンディションのグループ化¶
この機能はGroovyのObject.with
からインスパイアされました。これはSpecification.with
メソッドを使用することで、特定の対象オブジェクトに対するコンディションのグループ化が行えます。
def person = new Person(name: "Fred", age: 33, sex: "male")
expect:
with(person) {
name == "Fred"
age == 33
sex == "male"
}
インタラクションのグループ化¶
with
メソッドはインタラクションのグループ化にも使用できます。
def service = Mock(Service)
app.service = service
when:
app.run()
then:
with(service) {
1 * start()
1 * act()
1 * stop()
}
ポーリングコンディション¶
AsyncConditions
とBlockingVariable(s)
を統合した、非同期のコードをテストするためのユーティリティを追加しました。
def person = new Person(name: "Fred", age: 22)
def conditions = new PollingConditions(timeout: 10)
when:
Thread.start {
sleep(1000)
person.age = 42
sleep(5000)
person.name = "Barney"
}
then:
conditions.within(2) {
assert person.age == 42
}
conditions.eventually {
assert person.name == "Barney"
}
Eclipse用の実験的なDSLサポート¶
Groovy EclipseがSpockのDSLを理解しやすいように、Groovy EclipseためのDSLディスクリプタを同封するようにしました。このディスクリプタは、IDEによって自動的に検出され有効になります。以下に例を示します。
// currently need to type variable for the following to work
Person person = new Person(name: "Fred", age: 42)
expect:
with(person) {
name == "Fred" // editor understands and auto-completes 'name'
age == 42 // editor understands and auto-completes 'age'
}
他の例では以下のようになります。
def person = Stub(Person) {
getName() >> "Fred" // editor understands and auto-completes 'getName()'
getAge() >> 42 // editor understands and auto-completes 'getAge()'
}
DSLのサポートは、Groovy Eclipse 2.7.1以降で使用できます。これは、Groovy Eclipseの設定で無効にすることもできます。
IntelliJ IDEA用の実験的なDSLサポート¶
Intellij IDEAがSpockのDSLを理解しやすいように、Intellij IDEAためのDSLディスクリプタを同封するようにしました。このディスクリプタは、IDEによって自動的に検出され有効になります。以下に例を示します。
def person = new Person(name: "Fred", age: 42)
expect:
with(person) {
name == "Fred" // editor understands and auto-completes 'name'
age == 42 // editor understands and auto-completes 'age'
}
他の例では以下のようになります。
def person = Stub(Person) {
getName() >> "Fred" // editor understands and auto-completes 'getName()'
getAge() >> 42 // editor understands and auto-completes 'getAge()'
}
DSLのサポートはIntelliJ IDEA 11.1以降で使用できます。
Specificationクラスの分割¶
spock.lang.Specification
クラスの一部を、2つのスーパークラスに引き上げました。ひとつはspock.lang.MockingApi
で、これにはモックに関連する全てのメソッドが含まれます。そして、org.spockframework.lang.SpecInternals
には、直接使用することを意図していない内部メソッドが含まれます。
notThrown
とnoExceptionThrown
の失敗メッセージの改善¶
単に例外を通知する代わりに、Specification.notThrown
とSpecification.noExceptionThrown
は、以下のような失敗メッセージを表示するようになりました。
Expected no exception to be thrown, but got 'java.io.FileNotFoundException'
Caused by: java.io.FileNotFoundException: ...
HamcrestSupport.expect
¶
spock.util.matcher.HamcrestSupport
クラスに新たなexpect
メソッドを追加しました。これを使用することで、thenブロック内のHamcrestのアサーションが、より読みやすくなります。
when:
def x = computeValue()
then:
expect x, closeTo(42, 0.01)
@Beta¶
最近追加されたクラスや、メソッドには@Betaアノテーションが付与されている場合があります。これは今後、互換性のない変更が行われる可能性があることを表しています。さらにこれが、ユーザーからの貴重なフィードバックを得る機会になることも期待しています(あなたのフィードバックをお待ちしています!)。多くの場合、@Betaは1回、または2回のリリース内で削除されます。
0.6¶
モッキングの改善¶
モックフレームワークでは、いくつかのケースでよりわかりやすい診断メッセージを提供するようにしました。
また、複数のレスポンスの定義をチェーンできるようになっています。以下は、最初のbarメソッドの呼び出しにIOException
をスローし、 次の呼び出しでは数字の1、2、3を返し、それ以降の呼び出しにはRuntimeException
をスローします。
foo.bar() >> { throw new IOException() } >>> [1, 2, 3] >> { throw new RuntimeException() }
foo.bar(*_)
のように、任意の引数リスト(空のリストを含む)と、一致させることが可能になりました。
また、Hamcrestを使用して、引数制約を指定できるようになっています。
import static spock.util.matcher.HamcrestMatchers.closeTo
...
1 * foo.bar(closeTo(42, 0.001))
JUnitルールサポートの拡張¶
org.junit.rules.MethodRule
(これはJUnit 4.9でdeprecatedになりました)を実装したルールに加え、org.junit.rules.TestRule
インタフェースを実装したルールのサポートを追加しました。また、JUnitの新たな@ClassRule
アノテーションのサポートも追加しています。さらにルールの定義を自動的に認識し、明示的に初期化しなくても動作するようになっています。これはデフォルトコンストラクタを使用し、Spockが自動的にルールを初期化します。@Unroll
アノテーションを使用したネーミングパターンは、@TestName
ルールや、その他一般的なルールにも適用されるようになりました。
SpockのTestRuleサポートに関する 課題 240 の制約を確認してください。
コンディションの表示の改善¶
2つのオブジェクトを==
演算で比較すると、これらは等しないにもかかわらず、文字列表現はまったく同じものになってしまいます。このような場合に、オブジェクトの型を出力するようにしました。
enteredNumber == 42
| |
| false
42 (java.lang.String)
JUnitのフィクスチャアノテーション¶
従来のSpcokのフィクスチャメソッドに加え(または代わりに)、JUnitの@Before
、@After
、@BeforeClass
そして@AfterClass
を使用してフィクスチャメソッドが定義できるようになりました。
Tapestry 5.3のサポート¶
Howard Lewis Shipからのコントリビュートにより、TapestryモジュールがTapestry 5.3 互換となりました。従来の5.x以前のバージョンも、まだサポートされています。
IBM JDKのサポート¶
IBMのJDKで検証を行いバグを回避をすることで、IBMのJDKで問題なく動作するようになりました。
JUnit互換の改善¶
org.junit.internal.AssumptionViolatedException
のハンドリングを追加しました。これにより、JUnitのAssume関連のAPIが問題なく動作するようになります。また、@Unroll
を付与したメソッドが、IDE上で警告とならないようになりました。
@Unroll
の改善¶
@Unroll
のネーミングパターンがアノテーションの引数だけでなく、メソッド名で指定できるようになりました。
@Unroll
def "maximum of #a and #b is #c"() {
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 2 | 2
}
また、ネーミングパターンでプロパティアクセス、引数なしのメソッド呼び出しが使用できるようになっています。
@Unroll
def "#person.name.toUpperCase() is #person.age years old"() { ... }
さらに、@Unroll
アノテーションが、スペッククラスにも適用できるようにしました。この場合は、すべてのデータ駆動のフィーチャーメソッドが展開実行されます。
@Timeout
の改善¶
@Timeout
アノテーションが、スペッククラスにも適用にも適用できるようになりました。この場合は、@Timeout
がすでに設定されているフィーチャメソッド除き、すべてのフィーチャメソッドにタイムアウトが適用されます。タイムアウトが付与されたメソッドは、通常のテストフレームワークのスレッドで実行されます。これは、スレッドローカルの状態に依存しているテストを行う場合に重要になることがあります(たとえばGrailsのインテグレーションテスト)。また、タイムアウトを強制することができるチャンスをより多くするために、スレッド中断の動作が改善されました。
タイムアウトが発生した場合にスローされる例外の中に、テスト実行のスタックトレースが含まれるようになっています。これにより、どのスタックでタイムアウトになったのか、簡単に把握できるようになります。
データテーブルのシンタックス改善¶
テーブルのセルを論理和の記号(||
)で、区切ることができるようになりました。これは、入力値と出力の期待値を、視覚的に区別するために使用できます。
...
where:
a | b || sum
1 | 2 || 3
3 | 1 || 4
Groovy 1.8/2.0のサポート¶
Spock 0.6ではGroovy 1.7、1.8、そして2.0それぞれに対応した、3つのバージョンを提供しています。これらの中から正しいバージョンを使用するようにしてください。例えば、Groovy 1.8を使用している場合は、spock-core-0.6-groovy-1.8を使用する必要があります(他のモジュールも同様です)。Groovy 2.0のバージョンはGroovyの2.0-beta-3-SNAPSHOTをベースにしており、http://m2repo.spockframework.orgからのみ利用可能です。Groovy 1.7と1.8のバージョンはMavenのセントラルリポジトリからも利用可能です。また次のバージョンからは、Groovy 1.7をサポートしない予定です。
Grails 2.0のサポート¶
SpockのGrailsプラグインを別のプロジェクトにしました。http://github.spockframework.org/spock-grailsで管理しています。このプラグインは、Grailsの1.3と2.0の両方をサポートしています。
Spock Grails pluginはGrails 2.0の新たなテストミックスインをすべてサポートしています。これにより既存のユニットテスト関連のクラスが非推奨になります(例えばUnitSpec)。ただしインテグレーションテストのために、IntegrationSpecは引き続き使用する必要があります。
Intellij IDEA連携¶
JetBrains の開発メンバーにより、データテーブルの周りのいくつか便利な機能が追加されました。まず、コードを再フォーマットすると、データテーブルが自動的にレイアウトされます。データ変数はこれまでのように”unknown”とは表示されません。さらに、これらの型はテーブルの値から型が推論されます(なんと!)。
Githubリポジトリ¶
すべてのソースコードをhttp://github.spockframework.org/へ移動しました。ここにはGrails Spock plugin、Spock Exampleプロジェクト、そしてSpock Web ConsoleがGithubのプロジェクトとして置いてあります。また、各種プレゼンテーション用の、スライドやサンプルコードも参照可能になっています(例えばこれ)。
Gradleビルド¶
SpockをGradleで構築するようにしました。Spockを自分自身でビルドする場合は、GitHub repoをクローンし、gradlew build
を実行することで簡単にビルドできます。事前にビルドツールをインストールしておく必要はありません。Spockをビルドするために必要な前準備は、JDKのインストール(1.5以上)のみです。
移行ガイド¶
このページはバージョン間で行われた、互換性のない変更の内容とその対処方法について説明します。
0.6¶
クラスの初期化順序¶
注釈
これはSpecificationクラスを他のクラスから継承している場合に影響があります。
以下のスペックを例に説明します。
class Base extends Specification {
def base1 = "base1"
def base2
def setup() { base2 = "base2" }
}
class Derived extends Base {
def derived1 = "derived1"
def derived2
def setup() { derived2 = "derived2" }
}
0.5以前では、base1
、base2
、derived1
、derived2
の順にフィールドへの割り当てが行われていました。別の言い方をすると、フィールドの初期化は、同じクラスのsetupメソッドの直前に行われていました。これが0.6では、base1
、derived1
、base2
、derived2
の順に割り当てが行われます。これは、より一般的な順序であり、以前の動作で直面していたいくつかの問題を解決します。また、この変更により新たなJUnitのTestRuleのサポートを追加することができました。以下のコードは、0.6では正しく動作しません。
class Base extends Specification {
def base
def setup() { base = "base" }
}
class Derived extends Base {
def derived = base + "derived" // base is not yet set
}
この問題を回避するには、base
をフィールドで初期化するか、derived
への割り当てをsetupメソッドに移動するかのどちらかになります。
@Unroll
のネーミングパターンのシンタックス¶
注釈
これは0.5から変更はありませんが、0.6-SNAPSHOTから変更があります。
注釈
これは、Groovyの1.8と2.0のバージョンで影響を与えます。
0.5ではネーミングパターンはStringベースでした。
@Unroll("maximum of #a and #b is #c")
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 2 | 2
}
0.6-SNAPSHOTでは、これがGString
を返すクロージャに変更されました。
@Unroll({"maximum of $a and $b is $c"})
def "maximum of two numbers"() { ... }
様々な理由により、新しいシンタックスは期待したとおりに動作しませんでした。そのため、最終的には元のStringベースの指定に戻しています。また、このStringベースのシンタックス改善について、@Unrollの改善を参照してください。
Hamcrestマッチャーのシンタックス¶
注釈
これは、Groovyの1.7から、1.8または2.0のバージョンに移行するユーザーに影響します。
SpockはHamcrestのマッチャーを使用するために、非常に綺麗なシンタックスを提供しています。
import static spock.util.matcher.HamcrestMatchers.closeTo
...
expect:
answer closeTo(42, 0.001)
Groovy 1.7と1.8の間で行われた変更により、このシンタックスは多くの場合に正しく動作しなくなりました。例えば、次のコードは正しく動作しません。
expect:
object.getAnswer() closeTo(42, 0.001)
この問題を回避するには、HamcrestSupport.that
を使用します。
import static spock.util.matcher.HamcrestSupport.that
...
expect:
that answer, closeTo(42, 0.001)
Spockの将来のバージョンでは、おそらく前者のシンタックスを削除し、後者のシンタックスのサポートを強化していく予定です。