SpockとGroovyを使用したテストの概要
1. 前書き
この記事では、GroovyのテストフレームワークであるSpockについて見ていきます。 主に、Spockは、Groovyの機能を活用することにより、従来のJUnitスタックに対するより強力な代替手段を目指しています。
Groovyは、Javaとシームレスに統合するJVMベースの言語です。 相互運用性に加えて、動的である、オプションのタイプを持つ、メタプログラミングなどの追加の言語概念を提供します。
Spockは、Groovyを利用することで、Javaアプリケーションをテストする新しい表現力豊かな方法を導入します。これは、通常のJavaコードでは不可能です。 この記事では、Spockの高レベルの概念のいくつかを、いくつかの実用的なステップバイステップの例とともに説明します。
2. メーベン依存
始める前に、Maven dependenciesを追加しましょう。
org.spockframework
spock-core
1.0-groovy-2.4
test
org.codehaus.groovy
groovy-all
2.4.7
test
標準ライブラリと同じように、SpockとGroovyの両方を追加しました。 ただし、Groovyは新しいJVM言語であるため、コンパイルして実行できるようにするには、gmavenplusプラグインを含める必要があります。
org.codehaus.gmavenplus
gmavenplus-plugin
1.5
compile
testCompile
これで、Groovyコードで作成される最初のSpockテストを作成する準備が整いました。 GroovyとSpockはテスト目的でのみ使用しているため、これらの依存関係はテストスコープであることに注意してください。
3. スポックテストの構造
3.1. 仕様と機能
Groovyでテストを作成しているので、src/test/java.ではなくsrc/test/groovyディレクトリにテストを追加する必要があります。このディレクトリに最初のテストを作成して、Specification.groovy:という名前を付けましょう。
class FirstSpecification extends Specification {
}
Specificationインターフェースを拡張していることに注意してください。 フレームワークを使用可能にするには、各Spockクラスでこれを拡張する必要があります。 そうすることで、最初のfeature:を実装できるようになります
def "one plus one should equal two"() {
expect:
1 + 1 == 2
}
コードを説明する前に、Spockでは、featureと呼ばれるものは、JUnitでtestと呼ばれるものと同義であることに注意してください。 したがって、whenever we refer to a feature we are actually referring to a test.
それでは、featureを分析してみましょう。 そうすることで、JavaとJavaの違いをすぐに確認できるはずです。
最初の違いは、フィーチャメソッド名が通常の文字列として書き込まれることです。 JUnitでは、キャメルケースまたはアンダースコアを使用して単語を分離するメソッド名がありますが、これは表現力や人間が読めるものではありませんでした。
次は、テストコードがexpectブロックにあることです。 ブロックについては後ほど詳しく説明しますが、基本的にはテストのさまざまなステップを論理的に分割する方法です。
最後に、アサーションがないことを認識しています。 これは、アサーションが暗黙的であり、ステートメントがtrueに等しい場合に渡され、falseに等しい場合に失敗するためです。 繰り返しになりますが、アサーションについては後ほど詳しく説明します。
3.2. ブロック
JUnitのテストを作成するときに、テストをパーツに分割する表現的な方法がないことに気付く場合があります。 たとえば、ビヘイビア駆動開発をフォローしている場合、コメントを使用してgiven when thenパーツを示すことになります。
@Test
public void givenTwoAndTwo_whenAdding_thenResultIsFour() {
// Given
int first = 2;
int second = 4;
// When
int result = 2 + 2;
// Then
assertTrue(result == 4)
}
Spockはこの問題をブロックで解決します。 Blocks are a Spock native way of breaking up the phases of our test using labels.given when thenなどのラベルを提供します。
-
Setup(Givenによってエイリアス)–ここでは、テストを実行する前に必要なセットアップを実行します。 これは暗黙のブロックであり、コードはどのブロックにも含まれず、その一部になります
-
When –ここで、テスト対象にstimulusを提供します。 つまり、テスト対象のメソッドを呼び出す場所
-
Then –これはアサーションが属する場所です。 Spockでは、これらは単純なブールアサーションとして評価されます。これについては後で説明します
-
Expect –これは、同じブロック内でstimulusとassertionを実行する方法です。 表現力豊かなものに応じて、このブロックの使用を選択する場合としない場合があります
-
Cleanup –ここでは、他の方法では取り残されるテスト依存関係リソースを破棄します。 たとえば、ファイルシステムからファイルを削除したり、データベースに書き込まれたテストデータを削除したい場合があります。
今度はブロックを最大限に活用して、テストの実装をもう一度試してみましょう。
def "two plus two should equal four"() {
given:
int left = 2
int right = 2
when:
int result = left + right
then:
result == 4
}
ご覧のとおり、ブロックはテストを読みやすくするのに役立ちます。
3.3. アサーションにGroovy機能を活用する
Within the then and expect blocks, assertions are implicit。
ほとんどの場合、すべてのステートメントが評価され、trueでない場合は失敗します。 これをさまざまなGroovy機能と組み合わせると、アサーションライブラリの必要性をなくすことができます。 これを実証するために、listアサーションを試してみましょう。
def "Should be able to remove from list"() {
given:
def list = [1, 2, 3, 4]
when:
list.remove(0)
then:
list == [2, 3, 4]
}
この記事ではGroovyについて簡単に触れているだけですが、ここで何が起こっているのかを説明する価値があります。
まず、Groovyを使用すると、リストをより簡単に作成できます。 要素を角かっこで宣言するだけで、内部的にlistがインスタンス化されます。
次に、Groovyは動的であるため、defを使用できます。これは、変数の型を宣言していないことを意味します。
最後に、テストを簡素化するコンテキストで、実証された最も有用な機能は演算子のオーバーロードです。 これは、Javaのように参照比較を行うのではなく、内部的にequals()メソッドを呼び出して2つのリストを比較することを意味します。
テストが失敗したときに何が起こるかを示すことも価値があります。 それを壊してから、コンソールへの出力を確認しましょう。
Condition not satisfied:
list == [1, 3, 4]
| |
| false
[2, 3, 4]
at FirstSpecification.Should be able to remove from list(FirstSpecification.groovy:30)
2つのリストでequals()を呼び出すだけですが、Spockは、失敗したアサーションの内訳を実行するのに十分インテリジェントであり、デバッグに役立つ情報を提供します。
3.4. 例外の主張
Spockは、例外をチェックするための表現力のある方法も提供します。 JUnitでは、いくつかのオプションは、try-catchブロックを使用するか、テストの先頭でexpectedを宣言するか、サードパーティのライブラリを使用する可能性があります。 Spockのネイティブアサーションには、すぐに例外を処理する方法が付属しています。
def "Should get an index out of bounds when removing a non-existent item"() {
given:
def list = [1, 2, 3, 4]
when:
list.remove(20)
then:
thrown(IndexOutOfBoundsException)
list.size() == 4
}
ここでは、追加のライブラリを導入する必要はありませんでした。 もう1つの利点は、thrown()メソッドが例外のタイプをアサートしますが、テストの実行を停止しないことです。
4. データ駆動テスト
4.1. データドリブンテストとは何ですか?
基本的に、data driven testing is when we test the same behavior multiple times with different parameters and assertionsです。 この典型的な例は、数の二乗などの数学演算をテストすることです。 オペランドのさまざまな順列に応じて、結果は異なります。 Javaでは、パラメータ化されたテストという用語がよく知られています。
4.2. Javaでのパラメータ化されたテストの実装
状況によっては、JUnitを使用してパラメーター化されたテストを実装する価値があります。
@RunWith(Parameterized.class)
public class FibonacciTest {
@Parameters
public static Collection
ご覧のとおり、非常に多くの冗長性があり、コードはあまり読みやすくありません。 テストの外部に存在する2次元オブジェクト配列を作成する必要があり、さまざまなテスト値を挿入するためのラッパーオブジェクトも作成する必要がありました。
4.3. SpockでのDatatablesの使用
JUnitと比較した場合のSpockの簡単な利点の1つは、パラメータ化されたテストを実装する方法です。 繰り返しますが、スポックでは、これはData Driven Testing.として知られています。ここで、同じテストをもう一度実装しましょう。今回は、パラメーター化されたテストを実行するはるかに便利な方法を提供するData Tablesでスポックを使用します。 :
def "numbers to the power of two"(int a, int b, int c) {
expect:
Math.pow(a, b) == c
where:
a | b | c
1 | 2 | 1
2 | 2 | 4
3 | 2 | 9
}
ご覧のとおり、すべてのパラメーターを含む単純で表現力豊かなデータテーブルがあります。
また、テストと一緒に行うべき場所に属し、定型文はありません。 テストは表現力豊かで、人間が読める名前で、純粋なexpectおよびwhereブロックが論理セクションを分割します。
4.4. データテーブルが失敗したとき
テストが失敗したときに何が起こるかを確認することも価値があります。
Condition not satisfied:
Math.pow(a, b) == c
| | | | |
4.0 2 2 | 1
false
Expected :1
Actual :4.0
繰り返しますが、Spockは非常に有益なエラーメッセージを提供します。 Datatableのどの行が障害を引き起こしたか、そしてその理由を正確に確認できます。
5. モッキング
5.1. モックとは何ですか?
モッキングは、テスト対象のサービスが連携するクラスの動作を変更する方法です。 これは、依存関係を分離してビジネスロジックをテストできる便利な方法です。
この典型的な例は、ネットワーク呼び出しを行うクラスを、単にふりをするものに置き換えることです。 より詳細な説明については、this articleを読む価値があります。
5.2. スポックを使ったモック
Spockには独自のモックフレームワークがあり、GroovyによってJVMにもたらされた興味深い概念を利用しています。 まず、Mock:をインスタンス化します
PaymentGateway paymentGateway = Mock()
この場合、モックのタイプは変数のタイプによって推測されます。 Groovyは動的言語であるため、型引数を指定することもできます。これにより、特定の型にモックを割り当てる必要がなくなります。
def paymentGateway = Mock(PaymentGateway)
これで、PaymentGatewayモック,でメソッドを呼び出すと、実際のインスタンスが呼び出されることなく、デフォルトの応答が返されます。
when:
def result = paymentGateway.makePayment(12.99)
then:
result == false
これの用語はlenient mockingです。 つまり、定義されていないモックメソッドは、例外をスローするのではなく、適切なデフォルトを返します。 これは、モックを作成し、テストの脆弱性を軽減するための、Spockの設計によるものです。
5.3. Mocksでのスタブメソッド呼び出し
モックで呼び出されるメソッドを設定して、特定の方法で異なる引数に応答することもできます。 20:の支払いを行うときに、PaymentGatewayモックでtrueを返すようにしてみましょう。
given:
paymentGateway.makePayment(20) >> true
when:
def result = paymentGateway.makePayment(20)
then:
result == true
ここで興味深いのは、Spockがメソッド呼び出しをスタブ化するためにGroovyの演算子のオーバーロードをどのように利用するかです。 Javaでは、実際のメソッドを呼び出す必要があります。これはおそらく、結果のコードがより冗長で、表現力が低下する可能性があることを意味します。
それでは、さらにいくつかの種類のスタブを試してみましょう。
メソッド引数を気にするのをやめ、常にtrue,を返したい場合は、アンダースコアを使用できます。
paymentGateway.makePayment(_) >> true
異なる応答を交互に切り替えたい場合は、リストを提供できます。このリストに対して、各要素が順番に返されます。
paymentGateway.makePayment(_) >>> [true, true, false, true]
より多くの可能性があり、これらはモッキングに関するより高度な将来の記事でカバーされるかもしれません。
5.4. 検証
モックでやりたいことのもう1つは、期待されるパラメーターでさまざまなメソッドがモックで呼び出されたことをアサートすることです。 つまり、モックとの相互作用を検証する必要があります。
検証の一般的な使用例は、モックのメソッドにvoidの戻り値の型がある場合です。 この場合、操作する結果がないため、テスト対象のメソッドを介してテストするための推定動作はありません。 一般に、何かが返された場合、テスト対象のメソッドはそれを操作でき、その操作の結果が私たちが主張するものになります。
voidの戻り値の型を持つメソッドが呼び出されることを確認してみましょう。
def "Should verify notify was called"() {
given:
def notifier = Mock(Notifier)
when:
notifier.notify('foo')
then:
1 * notifier.notify('foo')
}
Spockは、Groovyオペレーターのオーバーロードを再び利用しています。 mocksメソッドの呼び出しに1を掛けることで、それが呼び出される回数を示しています。
メソッドがまったく呼び出されなかったか、指定された回数だけ呼び出されていなかった場合、テストでは有益なSpockエラーメッセージを表示できませんでした。 2回呼び出されたことを期待して、これを証明しましょう。
2 * notifier.notify('foo')
これに続いて、エラーメッセージがどのように表示されるかを見てみましょう。 いつものようにそれをします。それは非常に有益です:
Too few invocations for:
2 * notifier.notify('foo') (1 invocation)
スタブのように、より緩やかな検証マッチングも実行できます。 メソッドパラメータが何であるかを気にしない場合は、アンダースコアを使用できます。
2 * notifier.notify(_)
または、特定の引数で呼び出されていないことを確認したい場合は、not演算子を使用できます。
2 * notifier.notify(!'foo')
繰り返しますが、より多くの可能性がありますが、将来のより高度な記事でカバーされる可能性があります。
6. 結論
この記事では、Spockを使用したテストについて簡単に説明しました。
Groovyを活用することで、テストを通常のJUnitスタックよりも表現力豊かにする方法を示しました。 specificationsとfeaturesの構造について説明しました。
また、データ駆動型テストの実行がいかに簡単であるか、また、ネイティブのSpock機能を介してモックとアサーションがいかに簡単であるかを示しました。
これらの例の実装はover on GitHubにあります。 これはMavenベースのプロジェクトなので、そのまま実行するのは簡単です。