Spring×Spockのステレオタイプごとのテストまとめ

SpringにおけるSpockユニットテストについて、ステレオタイプ（Controller/Service/Respository）ごとに方針をまとめます。また、基本的なSpock部品の使用方針についてもまとめます。

今回使用するサンプルコードはGitHubにもあがっていますので合わせて参照ください。

- [spring-spock-sandbox｜GitHub](https://github.com/hondaYoshitaka/spring-spock-sandbox)

## 環境

|カテゴリ|ライブラリ|version|
|:----|:----|:----|
|Spring Testing|[spring-boot-starter-test](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html)|2.1.2.RELEASE|
|テストフレームワーク|[Spock](http://spockframework.org/spock/docs/1.2/all_in_one.html)|1.2-groovy-2.5|
|Mock|[Mockito](https://github.com/mockito/mockito)|2.23.4|
|DBテストデータ|[DBUnit](http://dbunit.sourceforge.net/)|2.6.0|

## Spock Blocks
Spock Blocksの基本内容については公式ドキュメント[Blocks｜spockwebframework](http://spockframework.org/spock/docs/1.3/spock_primer.html#_blocks)を参照ください。

### Blockごとの方針
以下の４種類のブロックを積極的に使用し、可読性の高いコード心がけます。

#### `given`
- 以下の役割に該当する前処理をまとめて記載する
    - パラメータの準備や加工
    - Mockの設定
    - Data Sourceへのデータ投入

#### `when`
- 常にシンプルに保つ
    - テスト対象のみを記載することで**テスト対象を明確に**する
    - この句では 絶対に前/後処理は書かない

#### `then`
- 検証項目には `assert` または `verifyAll` を付加する（後述）
- シンプルな検証を心がける
    - 複雑なロジックでの検証は避ける
    - 複雑になりそうな場合、ケースを分割して実施することを検討する

#### `where`
- 入力と期待値の間に区切り `||` を付加する

```groovy
param1 | param2 || expected
'1'    |'2'     || '期待値'
```

- 期待値を `then` 句にハードコードする場合、この句に期待値がないことを示すため `|| _` と書いておくとよい

```groovy
param1 | param2 || _
'1'    | '2'    || _
```

## アサーション機能の活用
### 単項目の検証 `assert`
単項目の検証には `assert` を付加することを推奨します。
というのも、Spockでは**ネストされた項目の検証**は検証対象としてみなされません。

```groovy
then:
actual.code == expectedCode // ここは検証項目と見なされる

for (def model : actual.subModels) {
    model.name == expectedName  // !! ここは検証項目と見なされない !!
}
```

「本テストでこの項目を検証したい」という意思を残すために付加することを推奨します。

```groovy
then:
assert actual.code == expectedCode

for (def model : actual.subModels) {
    assert model.name == expectedName 
}
```
### オブジェクト内の複数項目の検証 `verifyAll`
同一オブジェクト内の複数項目の検証には[Spockの機能である`verifyAll`](http://spockframework.org/spock/docs/1.2/all_in_one.html#_using_code_verifyall_code_to_assert_multiple_expectations_together)を活用します。
`verifyAll` はオブジェクト内の検証をまとめて行うための機能です。

検証したいオブジェクトを `verifyAll` の引数として渡します。
`verifyAll` 内では引数のオブジェクトはコンテキスト `it` として扱うことができます(`it`の省略も可)。

```groovy
then:
verifyAll(actual) {
    it.id == 999L
    it.name == 'name 1'
    it.price == 777
}
```

オブジェクトでの複数項目検証では有用なため活用しましょう。

## Mock

### 責務
> モックオブジェクトはテスト対象がコード内部で使用している呼び出し先と同じインターフェイスを持ち外からは正しく動くように見えるが、内部ロジックは実装されておらず**引数の検証とテストに必要な最低限の出力のみ**を行う。モックオブジェクトが未実装のクラスの振る舞いをまねることで、それに依存するクラス（開発中のクラス）の振る舞いを検証することができる。 ー [モックオブジェクト | IT media](http://www.itmedia.co.jp/im/articles/1111/07/news180.html) より

以下２つの責務に注目して、テストでのMockの書き方・方針を整理してみます。

- Mockオブジェクトに渡される引数の検証
- テストに必要な最低限の値の出力

#### 引数の検証
テスト対象内で呼ばれる処理に合わせ、具体的な引数を受け取る形式にしましょう！

```groovy:テストの実態にあったOKな例
Mockito.when(xxxService.fetchOne(1L))
```

中間処理でのバグを見逃してしまう可能性があるため、任意の引数をとる形式は不使用にすべきです。

```groovy:任意の引数を使ったNGな例
Mockito.when(xxxService.fetchOne(Mockito.any()))
                                 ~~~~~~~~~~~~~~
```

#### テストに必要な最低限の値の出力
使用されている変数やフィールドにのみ値を詰めたMock戻り値にしましょう！

```groovy:OKな例
def entity = new Entity(age: 28)

Mockito.when(/* ... */).thenReturn(entity)
```

参照するフィールドの誤りなどのバグ検知ができなくなるため、テストで使用していないフィールドには値を詰めるべきではありません。

```groovy:NGな例
// テスト対象のメソッドでは 実は`id`/`name`フィールドは参照されていない！
def entity = new Entity(id: 1L, name: 'name 1', age: 28)

Mockito.when(/* ... */).thenReturn(entity)
```

### Mockのインジェクション

```groovy
@InjectMocks
XXXService service
@Mock
XXXRepository xxxRepository
@Mock
YYYRepository yyyRepository
```

- `@InjectMocks`
    - Mockを差し込む対象のクラスを指定する
    - テスト対象クラスを指定する
- `@Mock`
    - Mockにするクラスを指定する

テスト全体のセットアップ処理で忘れずにモックを初期化します。

```groovy
@Unroll
class XXXServiceTest extends Specification {
    //...

    def setup() {
        MockitoAnnotations.initMocks(this)
    }
```

### TIPS: 引数の検証に関する諸注意
モック対象メソッドの引数クラス型に `equals` メソッドが実装されていないケースでは、引数の同一性がとれないためモックが期待通り動作しません。

```groovy:モックが期待通りに動かない例
def 'sample test'() {
    //...
    Mockito.when(xxxRepository.findBy(new SearchKeys(1L)).thenReturn(/* ... */)
                                     ~~~~~~~~~~~~~~~~~~~~
}

/**
 * `equals`未実装 (`Object#equals`が呼ばれる)
 */
// getter, setter and required-constructor
public static class SearchKeys {
    private final Long id;
}
```

私がテストを書いていて`equals`がなくてよく困ってたケースは[MyBatis3の`RowBounds`](https://github.com/mybatis/mybatis-3/blob/master/src/main/java/org/apache/ibatis/session/RowBounds.java)でした。

```groovy
Mockito.when(xxxRepository.findAll(new RowBounds(0, 10))
                                   ~~~~~~~~~~~~~~~~~~~~
```

このようなケースでは引数の検証部分を独自で作って上げる必要があります。具体的には`org.mockito.ArgumentMatcher`を実装したクラスを作成します。

私がサンプルとして作成した[RowBoundsMatcher](https://github.com/hondaYoshitaka/spring-spock-example/blob/master/src/test/java/com/github/hondaYoshitaka/mockito/RowBoundsMatcher.java)を使った場合、引数の検証部分を以下のように書くことで検証がうまくいくようになります。

```groovy
Mockito.when(xxxRepository.findAll(argThat(RowBoundsMatcher.of(0, 10))))
```

## ステレオタイプごとのテスト
Springで用意されているステレオタイプごとのテストの書き方を紹介します。

### ■ Controller
Springのテストコンポーネント `MockMvc` を利用し、実際にリクエストしてテストを行います。

#### ✔ 正常系

##### 前処理 `given`

ここで出てくる処理は大まかに２種類に分類されると思われます。

```groovy:クエリパラメータを用意する場合
given:
def params = new LinkedMultiValueMap<String, String>()
categoryId?.with { params.add('categoryId', it) }
price?.with { params.add('price', it) }
```

```groovy:Mockを用意する場合
given:
def responseDto = XXXFetchResponseDto.builder().id(999L).name('xxx name').price(888).build()
Mockito.when(xxxService.fetchOne(id)).thenReturn(responseDto)
```

- リクエストのPath Variable・Query Parameter・Bodyを用意する
- ビジネス層のモックを用意する

##### テスト内容 `when`

シンプルに、かつ、テスト対象を明確にするため、`MockMvc`を呼ぶ処理のみになるはずです。

```groovy:GETリクエスト
when:
def actual = mockMvc.perform(get("/api/xxx/${id}"))
        .andExpect(status().isOk())
        .andReturn()
```

```groovy:POSTリクエスト
when:
def actual = mockMvc.perform(post("/api/xxx")
        .contentType(MediaType.APPLICATION_JSON_UTF8)
        .content(objectMapper.writeValueAsBytes(params)))
        .andExpect(status().isCreated())
        .andReturn()
```

- HTTPステータスの検証を行う
    - 正常系なので `2xx` / `3xx` の検証になるハズ
- `MockMvc` からresponseを受け取る

##### 検証 `then`

```groovy
then:
def contents = objectMapper.readTree(actual.response.contentAsByteArray)

verifyAll(contents) {
    it.path('id').asLong() == 999
    it.path('name').asText() == 'xxx name'
    it.path('price').asInt() == 888
}
```

- responseからコンテンツ部分だけを抜き出し、Controllerの戻り値を検証する
    - 各項目が想定の値であること
    - サンプルは期待値をたまたまハードコードしているが、`where`句に記載するでも良い

##### テストパターン `where`

```groovy
where:
id || _
0L || _
```

- テストパターンがあれば追加する

#### ✔ 異常系 （バリデーションエラー)
##### 前処理 `given`
(正常系と同様)

##### テスト内容 `when`

```groovy
when:
def actual = mockMvc.perform(get("/api/xxx").params(params))
        .andExpect(status().isBadRequest())
        .andReturn()
```

- HTTPステータスの検証を行う
    - バリデーションエラーなので `400 Bad Request` の検証

##### 検証 `then`

```groovy
then:
def contents = objectMapper.readTree(actual.response.contentAsByteArray)
def errors = contents.path('errors').asList()

assert errors.size() == 1

verifyAll(errors[0]) {
    it.path('field').asText() == field
    it.path('code').asText() == code
}
```

- プロジェクトのAPIエラーパターンに応じたフィールドの検証をする
    - サンプルではエラーフィールドとコード

##### テストパターン `where`

```groovy
where:
categoryId | price     || field        | code
null       | '2'       || 'categoryId' | 'NotNull'
''         | '2'       || 'categoryId' | 'NotNull'
'1.0'      | '2'       || 'categoryId' | 'typeMismatch'
'aaa'      | '2'       || 'categoryId' | 'typeMismatch'
'1'        | '-2'      || 'price'      | 'Range'
'1'        | '-1'      || 'price'      | 'Range'
'1'        | '1000001' || 'price'      | 'Range'
'1'        | '1000002' || 'price'      | 'Range'
'1'        | '1.0'     || 'price'      | 'typeMismatch'
'1'        | 'aaa'     || 'price'      | 'typeMismatch'
```

- エラーになるパターンを網羅的に挙げる

### ■ Service
Requestやデータベースへのアクセスを行っていない層のため、ステレオタイプの中では一番シンプルなテストになります。

#### 前処理 `given`

```groovy:Mockを用意する場合
given:
def entities = [
        new XXX(id: 1L, name: 'name 1', price: 111),
        new XXX(id: 2L, name: 'name 2', price: 222)
]
Mockito.when(xxxRepository.findAllByCategory(categoryId, price))
        .thenReturn(entities)
```

#### テスト内容 `when`

```groovy
when:
def actual = xxxService.fetchAll(categoryId, price)
```

#### 検証 `then`

```groovy
then:
assert actual.xxxs.size() == 2

verifyAll(actual.xxxs[0]) {
    it.id == 1
    it.name == 'name 1'
    it.price == 111
}
verifyAll(actual.xxxs[1]) {
    it.id == 2
    it.name == 'name 2'
    it.price == 222
}
```

#### テストパターン `where`

```groovy
where:
categoryId | price || _
0L         | 100   || _
```

### ■ Repository

Repository層のテストでは以下の方式で行います。

- 実際にDatabaseにテストデータを登録し、検索を行う
- テスト終了後はテストデータをRollbackする
- 読み込むテストデータは外部ファイルには置かず、テストメソッド内で完結させる

#### Databaseテストデータの準備

テストに合わせてDatabaseのレコードを用意するため、今回のサンプルでは[DBUnit](http://dbunit.sourceforge.net/)を使用します。

予めデータを投入するメソッドを用意しておきます。
`org.dbunit.DataSourceDatabaseTester`(`tester`)クラスを使用してDatabaseに値を登録します。また、読み込みデータの表現には[Dbspock](https://github.com/yo1000/dbspock#for-dbunit)を使用します。

```groovy:xxxRepositoryTest
/**
 * databaseレコードをセットアップします.
 * --------------------------------------------
 * - データの形式は `com.yo1000.dbspock.dbunit` に準ずる
 *
 * @param data データセット
 */
private void prepareDataSet(final Closure data) {
    tester.dataSet = DbspockLoaders.loadDataSet(data)
    tester.onSetup()
}
```

このメソッドは前処理`given`時に使用します。

#### 前処理 `given`

```groovy:テストデータを用意する場合
given:
prepareDataSet({
    xxx {
        id | name | category_id | price | active
        0L | 'name 0' | 0L | 0 | false
        1L | 'name 1' | 10L | 100 | true
        2L | 'name 2' | 20L | 200 | true
    }
})
```

[Dbspock](https://github.com/yo1000/dbspock#for-dbunit)形式のデータセットを用意します。

- テーブル名を指定する
    - サンプルでは `xxx`
- カラムを定義する
    - サンプルでは `id | ... | active`
- カラムに対応するレコードを用意する

#### テスト内容 `when`

```groovy
when:
def actual = xxxRepository.findOne(key)
```

#### 検証 `then`

```groovy
then:
verifyAll(actual) {
    it.id == _id
    it.name == _name
    it.categoryId == _categoryId
    it.price == _price
    it.active == _active
}
```

- 検索結果エンティティから対象のフィールドを検証する

#### テストパターン `where`

```groovy
where:
key || _id | _name    | _categoryId | _price | _active
0L  || 0L  | 'name 0' | 0L          | 0      | false
1L  || 1L  | 'name 1' | 10L         | 100    | true
```

- 用意したテストデータに応じた検索キーと結果をマッピングする