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、そしてMavenを使用してビルドできます。Gradleのビルドは、Gradleの起動に必要なファイルを自動で取得します。これにより、1コマンドでEclipse、またはIDEAの環境を構築できます。詳しくはREADMEを参照してください。
次のステップ¶
この後のセクションでは、さまざまな環境下でどのようにSpockを使用するのか説明していきましょう。
- Ant
- Gradle
- Maven
Groovy コンソール
- Eclipse
- IDEA
データ駆動テスト¶
入力値と期待する結果の組み合わせを検証するため、同じテストコードを複数回実行したいことがあります。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
}
}
この方法は、このような簡単な状況ではよい方法ですが、いくつかの問題点があります。
コードとデータが混在していて、簡単にどちらかを変更できない
データを簡単に自動生成したり、外部リソースを読み込んだりできない
同じコードを複数回実行する場合は、コードを複製するか別メソッドに抽出をする必要がある
実行が失敗した場合、失敗を引き起こした入力値がすぐに分からない
コードを複数回実行するために、実行するメソッドを分けるというやり方は、賢いやり方ではない
データ駆動テストのサポートは、この問題を解決します。はじめに上記のコードを、データ駆動のフィーチャメソッドへリファクタリングをしましょう。まず、ハードコーディングされている3つの整数値を、メソッドの引数(データ変数と呼びます)に置き換えます。
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
文字列以外の値(例えば上記の#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が受信できたか」です。この問いに答えるには、publisherとsubscriberのやり取りを監視しするSubscriber
の特別な実装が必要です。この特別な実装はよくモックオブジェクトと呼ばれます。
subscriberのモック実装を自前で作成することもできます。しかし、メソッドの数が多かったり、相互作用が複雑なメソッドが増えてくると、このコードを書いたりメンテナンスするのが煩わしくなります。そこで、モックフレームワークの出番です。このフレームワークは、仕様対象のオブジェクトとコラボレータの間に期待する相互作用を宣言する方法を提供します。さらに、この期待する相互作用を検証する、コラボレータのモック実装を生成できます。
Javaの世界では、JMock、EasyMock、Mockitoといった、人気があり成熟したモックフレームワークに事欠きません。これらのモックフレームワークを、Spockと共に使用することもできます。しかし、Spockの言語仕様に合うように、モックフレームワークをSpock自身で再開発することを決めました。この判断は、Groovyの能力を最大限活用して、より簡単に相互作用中心のテストを書けるようにし、より読みやすく、また最高に楽しいものにしたいという思いからです。この章を読み終えた時に、その目標が達成されていると感じていただけると幸いです。
特に明示していない限り、モックフレームワークのすべての機能はJavaコードとGroovyコードのテスト両方で使用できます。
モックオブジェクトの作成¶
モックオブジェクトはMockingApi.Mock()
メソッドで作成します[1]。2つのsubscriberモックを作成してみましょう。
def subscriber = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)
または、Javaに近いシンタックスもサポートしています。これは、IDEのサポートがより受けやすくなるかもしれません。
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)¶
モッキングとは、仕様対象のオブジェクトとそのコラボレータ間の、(必須の)インタラクション(訳注: ここまで相互作用と表記してきましたが、Spockではインタラクションを用語として使用しているため、以降インタラクションと表記します)を宣言する活動です。次の例を見てください。
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 すべき」になります。(訳注: コードが英語の文章のように読めると言っている)
このフィーチャメソッドを実行すると、when:
ブロック内で発生するモックオブジェクトへのすべての呼び出しが、then:
ブロックで宣言したインタラクションと照合されます。もし、インタラクションが1つでも満たされない場合はInteractionNotSatisfiedError
(のサブクラス)が投げられます。この検証は自動的に行われます。開発者自身で検証コードを記述する必要はありません。
インタラクション¶
それでは、then:
ブロックについて詳しく見ていきましょう。上記の例では2つのインタラクションを宣言しています。この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
といったメソッドで宣言できるという意味です。同様に、ヘルパークラスのメソッドで宣言するといったことも可能です。
モックオブジェクトへの呼び出しが発生すると、宣言した順番でインタラクションを照合します。もし、呼び出しが複数のインタラクションにマッチする場合は、インタラクションが実行上限に達してない限り、先に宣言したインタラクションが優先されます。ただし1つ例外があります。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:
ブロック内でのインタラクション宣言を可能にしているのでしょうか? 答えはSpockの内部にあります。Spockは、then:
ブロックのインタラクション宣言を、when:
ブロックを処理する直前へ移動します。これは多くの場合に問題なく動作します。しかし、特定の条件下では問題が起きます。
when:
publisher.send("hello")
then:
def message = "hello"
1 * subscriber.receive(message)
ここでは、期待する引数を変数に受けています(多重度を変数に受ける場合も同様)。しかし、Spockはインタラクションが変数宣言へ参照を持っていると把握するほど賢くありません。そのため、インタラクションをそのまま移動し、実行時に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)
なぜマッチした呼び出しが多すぎたのか解析しやすいように、Spockは問題となるインタラクションにマッチしたすべての呼び出しを表示します(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")
の呼び出しの1つがTooManyInvocationsError
を引き起こしています。ここで、subscriber.receive("hello")
のような2つの呼び出しは、1行に集約されて表示されます。最初のsubscriber.receive("hello")
はreceive("goodbye")
の前に発生していたかもしれませんが、この出力からは区別できません。
2つめの場合(意図したよりも実行が少ない場合)は、when
ブロックの実行が完了した時点でのみ検出できます(終了するまでは呼び出しの可能性があるため)。検出した場合はTooFewInvocationsError
を発生させます。
Too few invocations for:
1 * subscriber.receive("hello") (0 invocations)
これはメソッドの呼び出しが、一切なかったということではありません。同じメソッドが、他の引数で呼び出された、異なるモックオブジェクトで呼び出された、もしくは、”別の”メソッドが呼び出された、といったことが考えられます。これらいずれの場合でも、TooFewInvocationsError
が発生します。
なぜ意図しない呼び出しが代わりに起きたのか解析しやすいよう、Spockはどのインタラクションにもマッチしなかったすべての呼び出しを表示します。この呼び出しは、問題のあるインタラクションに類似している順番で表示されます(new in Spock 0.7)。特に、インタラクションの引数以外がすべて一致する呼び出しが最初に表示されます。
Unmatched invocations (ordered by similarity):
1 * subscriber.receive("goodbye")
1 * subscriber2.receive("hello")
実行順序¶
多くの場合に、正確なメソッドの実行順序に意味はありません。また、この実行順序は時間と共に変わる可能性があります。over-specificationを避けるため、Spockはデフォルトで、最終的に宣言したインタラクションすべてが満たされれば、どのような実行順序も許容します。
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の将来のバージョンでは、おそらく前者のシンタックスを削除し、後者のシンタックスのサポートを強化していく予定です。