KotlinでSpock入門[Data Driven Testを学んでデータ駆動テストを使い倒す2]

この記事は筆者のソロ　[Advent Calendar 2022](https://qiita.com/advent-calendar/2022/panda) 4日目の記事です。

引き続きSpockの使い方を紹介していきます。今回は[前回](https://qiita.com/JY8752/private/e78f7369dff3166b3bb9)の続きでDataDrivenTestについて書いていきます。

[KotlinでSpock入門[基礎編]](https://qiita.com/JY8752/items/5106a268de00a0daa4db)
[KotlinでSpock入門[基礎編2]](https://qiita.com/JY8752/items/c4f7cb585bb91236b975)
[KotlinでSpock入門[Data Driven Testを学んでデータ駆動テストを使い倒す]](https://qiita.com/JY8752/items/e78f7369dff3166b3bb9)
[KotlinでSpock入門[Data Driven Testを学んでデータ駆動テストを使い倒す2]](https://qiita.com/JY8752/items/bc2b1b14fdab36be2570)　<- 今ここ
[KotlinでSpock入門[mock編]](https://qiita.com/JY8752/items/0bb87522f36a371421c1)
[KotlinでSpock入門[Spring boot実践編]](https://qiita.com/JY8752/items/99ef0c0376b7a2d65510)

記事作成で使用したサンプルコードはこちら

https://github.com/JY8752/spock-demo

# 多変数データパイプ
前回データテーブルだけでなくデータパイプをwhereブロックに指定することでデータ変数に値を注入できることを述べたが、DBのデータを直接注入したりもできる。

```groovy
@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")

def "maximum of two numbers"() {
  expect:
  Math.max(a, b) == c

  where:
  [a, b, c] << sql.rows("select a, b, c from maxdata")
}
```

使用しない変数はアンダーラインで使用しないようにできる。

```
where:
[a, b, _, c] << sql.rows("select * from maxdata")
```

# データパイプの名前付き分解
 パイプは以下のように分解することで見やすくしたりもできる。
```groovy
    def "multi deta pipe test"() {
        expect:
        a + b == c
        where:
        [a, b, c] << [
            [
                    a: 1,
                    b: 3,
                    c: 4
            ],
            [
                    a: 2,
                    b: 4,
                    c: 6
            ]
        ]
    }
```

# データ変数への値の代入
データ変数には値を直接代入することもできる。
```groovy
    def "test data variable assignment"() {
        expect:
        a + b == c
        where:
        a = 3
        b = 1
        c = 4
    }
```
実際は以下のようにDBから取得したデータを変数に直接代入したりするときに使える。
```groovy
where:
row << sql.rows("select * from maxdata")
// pick apart columns
a = row.a
b = row.b
c = row.c
```

# 別のデータ変数へのアクセス
別のデータ変数にアクセスする方法は2つ。
１つは上述したような直接代入したデータ変数は別の箇所で使用することができる。

```groovy
    def "test data variable assignment"() {
        expect:
        a + b == c
        where:
        value << [1, 2, 3]
        a = value
        b = value + 1
        c = a + b
    }
```
２つ目はデータテーブル内の以前のカラムにアクセスする方法である。また、同じwhereブロック内の過去のデータテーブルの列も含まれるため以下のようにも書ける。

```groovy
    def "test Accessing Other Data Variables"() {
        expect:
        b - a == 1
        c + d == e
        where:
        a|b
        1| a + 1
        2| a + 1
        3| a + 1
        and:
        c = 1
        and:
        d | e
        b | d + 1
        b | d + 1
        b | d + 1
    }
```

# データ変数への分割代入
反復処理できるオブジェクトであればパイプを使わず以下のように標準的なGroovyの複数代入の構文を使用することもできる。

```groovy
@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")

def "maximum of two numbers multi-assignment"() {
  expect:
  Math.max(a, b) == c

  where:
  row << sql.rows("select a, b, c from maxdata")
  (a, b, c) = row
}
```

# データテーブル、パイプ、データ変数への代入の組み合わせ
以下のように複数組み合わせることも可能。
```groovy
where:
a | b
1 | a + 1
7 | a + 2
0 | a + 3

c << [3, 4, 0]

d = a > c ? a : c
```

# 型変換
featureメソッドのパラメーター引数に型宣言をすることで暗黙的にデータ変数への型変換を行える。以下の例ではIntegerで宣言したデータ変数iにwhereブロックで文字列を注入しているがexpectブロックではちゃんと型変換されIntegerとなっている。
```groovy
    def "type coercion for data variable values"(Integer i) {
        expect:
        i instanceof Integer
        i == 10

        where:
        i = "10"
    }
```

また、@Useアノテーションを使用しコンバーターのようなものを適用することもできる。以下の例ではBar型で宣言されたデータ変数にBaz型の値を注入しているがexpectブロックではBar型の値になっている。これを実現しているのはBaz型をBar型に変換する```CoerceBazToBar```というクラスであり、これを@Useに指定している。

```groovy
@Use(CoerceBazToBar)
class Foo extends Specification {
    def foo(Bar bar) {
        expect:
        bar == Bar.FOO

        where:
        bar = Baz.FOO
    }
}
enum Bar { FOO, BAR }
enum Baz { FOO, BAR }
class CoerceBazToBar {
    static Bar asType(Baz self, Class<Bar> clazz) {
        return Bar.valueOf(self.name())
    }
}
```

# テストケースの命名
Spockの反復テストはデフォルトで以下のような表示になります。
```groovy
    def "maximum of two numbers7"() {
        expect:
        Math.max(a, b) == c

        where:
        a ; b ;; c
        1 ; 3 ;; 3
        7 ; 4 ;; 7
        0 ; 0 ;; 0
    }
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551753/b1a0d37e-5e6f-aada-292c-967efb9c2840.png)

データ変数をメソッド名の中で使用することができ、使用するには#変数名の形式で使用することができる。

```groovy
    def "when num1: #a num2: #b, max is #c"() {
        expect:
        Math.max(a, b) == c

        where:
        a ; b ;; c
        1 ; 3 ;; 3
        7 ; 4 ;; 7
        0 ; 0 ;; 0
    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551753/52710ca4-80e7-d27c-8488-43d220452b9c.png)

展開する変数はオブジェクトでも可能で、プロパティアクセスや引数なしのメソッド呼び出しもかのう。
```groovy
def "#person is #person.age years old"() { // property access
def "#person.name.toUpperCase()"() { // zero-arg method call
```

以下のような引数ありのメソッド呼び出しや計算式などはNG。
```groovy
def "#person.name.split(' ')[1]" {  // cannot have method arguments
def "#person.age / 2" {  // cannot use operators
```

もし、上記のような複雑な処理の値を使用したい場合は以下のように変数を追加することもできる。
```groovy
def "#lastName"() {
  ...
  where:
  person << [new Person(age: 14, name: 'Phil Cole')]
  lastName = person.name.split(' ')[1]
}
```

組み込みのデフォルトで使える以下の変数もある。
- #featureName featureメソッド名が展開されるので全て同じ名前で表示したい場合に使う。
- #iterationIndex イテレーションのインデックス番号を展開できる。
- #dataVariables ```x: 1, y: 2, z: 3```のようにデータ変数を展開する。
- #dataVariablesWithIndex ```x: 1, y: 2, z: 3, #0```のようにインデックス付きでデータ変数を展開する。

@Unrollアノテーションを使用して以下のようにも書ける。

```groovy
    @Unroll("#featureName[#iterationIndex] #a + #b = #c")
    def "maximum of two numbers7"() {
        expect:
        Math.max(a, b) == c

        where:
        a ; b ;; c
        1 ; 3 ;; 3
        7 ; 4 ;; 7
        0 ; 0 ;; 0
    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551753/8a03dfd4-7912-3ba1-27c8-d9c361c9c1cb.png)

# まとめ
前回Data Driven Testについての基礎的な内容を紹介しましたが、今回は以下の内容を紹介しました。
- DBなどの外部リソースからのデータの注入方について。
- データ変数のさまざまな取り扱いの仕方について。
- featureのさまざまな命名の方法について。

筆者は初めてJUnitでパラメーターテストを知ったときはこんなテストの書き方があるんだとめちゃくちゃ感動しました。Spockはこのようなデータ駆動的なテストが簡潔に書きやすいと感じました！今回は以上です！