はじめに
本記事は、JavaおよびGroovy向けのユニットテストフレームワークであるSpockのリファレンスの内容の個人的メモです
https://spockframework.org/spock/docs/2.5-SNAPSHOT/index.html
基本の文法
import spock.lang.*
class MyFirstSpecification extends Specification {
// fields
// Sharedフィールドを用いて共通のフィールドを定義できる
@Shared res = new VeryExpensiveResource()
// fixture methods
// feature methods
def "pushing an element on the stack"() {
// blocks go here
}
// helper methods
}
-
@Sharedフィールド- パフォーマンス向上とリソース節約のために使用
- 複数スレッドからアクセス可能
- テストケースごとにインスタンス化されず、使いまわされる
- パフォーマンス向上とリソース節約のために使用
フィクスチャメソッド
def setupSpec() {} // runs once - before the first feature method
def setup() {} // runs before every feature method
def cleanup() {} // runs after every feature method
def cleanupSpec() {} // runs once - after the last feature method
ブロック
- given: セットアップを行う
- when: 複数定義可能。任意のコードを含めることが可能
- then:
- 複数定義可能。条件・例外条件・相互作用のみしか記述できない
- 全てのトップレベルの記載は暗黙的に条件文として評価される
then:
!stack.empty
stack.size() == 1
stack.peek() == elem
thrown(EmptyStackException)
// or
EmptyStackException e = thrown()
notThrown(EmptyStackException)
- expect: 条件を記述。条件と変数の定義のみしか含めることができない。thenよりexpectの仕様が推奨される
- cleanup: whereブロックの後にしか配置できない。リソースの解放等のために利用されるが、通常記載する必要はない
- where: データ駆動型のテストメソッドを記述するために利用される
- and: ブロックを分割し、:の後に説明を記載することにより、可読性を高める
- ※then,expectブロック以外で条件を記載することはできない。記載したい場合、groovyのassertを使う必要がある
ヘルパーメソッド
- ヘルパーメソッドを用いた判定の分離
- メソッドの戻り値はvoidとし、
@Validを付与して各条件について結果が出力されるようにする必要がある
@Verify void matchesPreferredConfiguration(PC pc) { pc.vendor == "Sunny" pc.clockRate >= 2333 pc.ram >= 4096 pc.os == "Linux" } - メソッドの戻り値はvoidとし、
-
with(sut){...}を用いた検証文の簡略化- ヘルパーメソッドを用いる場合と同様の判定をwithを用いて行うこともできる
- コードの可読性向上・構造化・デバッグログの詳細化と言ったメリットがある
when: def pc = shop.buyPc() then: with(pc) { vendor == "Sunny" clockRate >= 2333 ram >= 406 os == "Linux" } -
verifyAll(sut){...}を用いたソフトアサーション- 1つのテストメソッド内で複数のアサーションを実行し、途中で失敗しても全てのアサーションを評価してから結果をまとめて報告する機能
- ヘルパーメソッドの利用は慎重にすべき
最後にアドバイスです。コードの再利用は一般的に良いことですが、やり過ぎには注意が必要です。フィクスチャやヘルパーメソッドの使用は、機能メソッド間の結合度を高める可能性があることに注意してください。再利用しすぎたり、不適切なコードを再利用したりすると、脆弱で進化が困難な仕様になってしまいます。
データ駆動テスト
- データ駆動テストが解決する懸念としては、以下のようなものがある
- コードとデータの混在
- データの自動生成や外部ソースからの取得が困難
- 同じコードの複数回実行(コードを複製するか、別のメソッドに抽出する必要がある)
- 障害が発生した場合、どの入力が障害の原因となったのかがすぐには分からない
- 同じコードを複数回実行しても、別々のメソッドを実行する場合と同じ分離の恩恵を受けられない
- whereブロックを用いたデータ駆動テストのアプローチ
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 3 | 3
7 | 4 | 7
0 | 0 | 0
// または
where:
_____
a | _ // _は無視される(最低2つ以上の列が必要なため記載している)
1 | _ // |は、入力と期待値を区別するため||としても問題ない
7 | _
0 | _
_____
b | c
1 | 2
3 | 4
5 | 6
// またはデータパイプを用いてデータを指定できる
where:
a << [1, 7, 0]
-
@Unrollを付与:データ駆動テストの各ケースを個別のテストとして表示 -
@Rollupを付与:データ駆動テストの全ケースを1つのテストとしてまとめて表示し、全ての行が成功した場合のみ成功と報告
インタラクションベースのテスト
- インタラクションベースのテストとは、モックを使用したテストのこと
- javaで用いられる著名なモック
- JMock、 EasyMock、Mockito
- ほとんどのJavaモックフレームワークと同様に、Spockは JDK動的プロキシ(インターフェースのモック作成時)とByte BuddyまたはCGLIBプロキシを用いてモックを実現する
- Byte Buddy: 新しい。生成速度が高速
- CGLIB: 古い。Java8までしか対応していない。finalクラスのモックが不可
- モックの内部的な仕組み
- Byte Buddyでサブクラス生成
- 元メソッドをオーバーライド
- モック動作を定義したハンドラーに処理を委譲
- 有名なモックFWが用いている内部技術(参考)
- Mockito 3.0+: Byte Buddy使用(CGLIBから移行)
- Spock 2.0+: Byte Buddy使用
- Spring Boot Test: Byte Buddy使用
- javaで用いられる著名なモック
モックの定義方法
def subscriber = Mock(Subscriber)
// または
Subscriber subscriber = Mock()
// コードへのモック設定方法
def setup() {
publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
publisher.subscribers << subscriber2
// 検証方法
then:
1 * subscriber.receive("hello")
}
- 許可される検証方法については、下記参照
スタブ
- スタブ実装の例
subscriber.receive(_) >> "ok"
// 連続した呼び出しの順番ごとに異なる値を返す場合
subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
// 動的に戻り値を計算して返す(>が2つであることに注意)
subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }
// 連結させて記載することも可能(4回目にエラーをスローし、その後はokを返す)
subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"
// モックとスタブを組み合わせて使用することも可能
1 * subscriber.receive("message2") >> "fail"
// Stub()メソッドを用いてスタブ定義する(モックとしては使用できない)
Subscriber subscriber = Stub()
// 宣言時にインタラクションを定義できる(モックでも可能)
Subscriber subscriber = Stub {
receive("message1") >> "ok"
receive("message2") >> "fail"
}
- スタブとモックの違いとは?
- スタブ:テスト対象に決まった値を返すためのもの
- モック:メソッドが正しく呼ばれたかを検証するためのもの(インタラクションの検証)
- Groovyのスタブを用いた方がいいケース
- 予期しない呼び出しに関して柔軟に応答したい場合
- スタブの場合、明示的に戻り値を定義していない場合でも、デフォルト値等を返してくれる
- 予期しない呼び出しに関して柔軟に応答したい場合
プリミティブ型の場合、プリミティブ型のデフォルト値が返されます。
非プリミティブな数値 (などBigDecimal) の場合、ゼロが返されます。
値がスタブインスタンスから割り当て可能な場合は、インスタンスが返されます(例:ビルダーパターン)
数値以外の値の場合、「空」または「ダミー」のオブジェクトが返されます。これは、空の文字列、空のコレクション、デフォルトコンストラクタから構築されたオブジェクト、またはデフォルト値を返す別のスタブのいずれかを意味します。org.spockframework.mock.EmptyOrDummyResponse詳細はクラスを参照してください。
スパイ
- スパイは、インタラクションを定義していない呼び出しに関しては、実際のオブジェクトを呼び出す
- スパイの定義方法
SubscriberImpl subscriber = Spy(constructorArgs: ["Fred"])
// 実際のメソッドを明示的に呼び出す
subscriber.receive(_) >> { String message -> callRealMethod(); message.size() > 3 ? "ok" : "fail" }
拡張機能
-
SpockConfig.groovyで定義する。ファイルの場所は以下の順に参照される- システムプロパティ(
spock.configuration) - テスト実行クラスパスのルート
- Spockユーザーホーム(デフォルト:
~/.spock/)
- システムプロパティ(
-
@Ignore- テストの無視 -
@IgnoreRest- 特定テストのみ実行(Ignoreの反対) -
@IgnoreIf- 条件付き無視 -
@Requires- 条件付き実行 -
@PendingFeature- 未実装機能(成功したらエラーになる)
ユーティリティ
-
MutableClockにより、基準時刻(現在時刻)を指定し、時間を用いるテストを安定化させることが可能