Groovy (Spock) で Java のテストをするときのテストデータ作成のTips

Java のプログラムのテストを Groovy (spock) で書くのは昔からやっているのですが、テストデータを書き下す際の工夫をメモします。

## テスト対象プログラム

```FirstDate.java
package example;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

public class FirstDate {
    /**
     * テスト対象メソッド。
     * 与えられた日付のリストから一番過去のものを返す
     */
    public static Date firstDate(List<Date> dates) {
        List<Date> copy = new ArrayList<>(dates);
        copy.sort(Comparator.<Date>naturalOrder());
        return copy.get(0);
    }
}
```
この `firstDate` メソッドをテストしたいとします。

```FirstDateSpec.groovy
package example

import spock.lang.Specification

class FirstDateSpec extends Specification {
    // ここにテストメソッドを足していきます
}
```

ここにテストメソッドを足していきます。

## 普通のやり方

```groovy
def "基本形"() {
    given:
    def dates = [new Date('2015/5/5'), new Date('2015/1/1'), new Date('2015/10/10'), new Date('2015/11/3')]

    when:
    def firstDate = FirstDate.firstDate(dates)

    then:
    firstDate == new Date('2015/1/1')
}
```

`new Date` が沢山でてきてうっとおしいですね。この邪魔な `new Date` を削減していくことを目指します。

## `new Date` を `as Date` できるようにする

```groovy
def setup() {
    String.metaClass.define {
        def originalAsType = String.metaClass.getMetaMethod('asType', [] as Class[])
        asType = { Class c ->
            if (c == Date) {
                new Date(delegate)
            } else {
                originalAsType.invoke(delegate, c)
            }
        }
    }
}

def "asで変換"() {
    given:
    def dates = ['2015/5/5' as Date, '2015/1/1' as Date, '2015/10/10' as Date, '2015/11/3' as Date]
    
    when:
    def firstDate = FirstDate.firstDate(dates)
    
    then:
    firstDate == '2015/1/1' as Date
}
```

Groovy パワーで `String` を `as` で `Date` にできるようにしてみましたが、あんまり見た目は変わりませんね。
でもこの as は下準備です。

## `collect` で `List<String>` を `List<Date>` にする1
```groovy
def "collectで変換1"() {
    given:
    def dates = ['2015/5/5', '2015/1/1', '2015/10/10', '2015/11/3'].collect { new Date(it) }

    when:
    def firstDate = FirstDate.firstDate(dates)

    then:
    firstDate == '2015/1/1' as Date
}
```

一気に邪魔者が減りましたね。

## `collect` で `List<String>` を `List<Date>` にする2

```groovy
def "collectで変換2"() {
    given:
    def dates = ['2015/5/5', '2015/1/1', '2015/10/10', '2015/11/3'].collect { it as Date }

    when:
    def firstDate = FirstDate.firstDate(dates)

    then:
    firstDate == '2015/1/1' as Date
}
```
クロージャの中でも as を使ってみました。

## `collect`の変換クロージャを外出し

```groovy
def "collectの変換クロージャを外出しして変換"() {
    def asDate = { it as Date }

    given:
    def dates = ['2015/5/5', '2015/1/1', '2015/10/10', '2015/11/3'].collect asDate

    when:
    def firstDate = FirstDate.firstDate(dates)

    then:
    firstDate == '2015/1/1' as Date
}
```

クロージャを変数に入れてみました。`List<Date>` を沢山作りたいときはちょっと簡潔になるかもしれません。なお、この例の場合でクロージャ部分が `{new Date(it)}` だと it の型がわからないという警告が出ます。

## static method で変換

```groovy
static <T> List<T> memberAs(List<String> l, Class<T> c) {
    return l.collect({ it.asType(c) })
}

def "staticMethodで変換"() {
    given:
    def dates = memberAs(['2015/5/5', '2015/1/1', '2015/10/10', '2015/11/3'], Date)

    when:
    def firstDate = FirstDate.firstDate(dates)

    then:
    firstDate == '2015/1/1' as Date
}
```

こういう書き方もできますが、memberAs の呼び出し方がなんとなくダサさを醸し出しています。
この呼び出しは Groovy のパワーでダサくなくす方法があります。

## Category と use で変換

```groovy
@Category(List)
class ListMemberAs {
    public <T> List<T> memberAs(Class<T> c) {
        return this.collect({ it.asType(c) })
    }
}

def "Categoryで変換"() {
    given:
    List<Date> dates = null
    use(ListMemberAs) {
        dates = ['2015/5/5', '2015/1/1', '2015/10/10', '2015/11/3'].memberAs(Date)
    }

    when:
    def firstDate = FirstDate.firstDate(dates)

    then:
    firstDate == '2015/1/1' as Date
}
```

Groovy の Category を使えばリストにメソッドを追加したように見せかけることができます。
ただし、呼び出ししている部分を `use` とクロージャで囲う必要があり、 dates の宣言部分などにダサさが残ります。
なお `given`, `when`, `then` を含む全体を use で囲うと spock のテストメソッドとして認識されなくなります。
この例にはそぐわないですが、Groovy の Category と use には可能性を感じます。

## Category を Use アノテーションで適用

```groovy
import spock.util.mop.Use

@Use(ListMemberAs)
def "CategoryをUseで適用して変換"() {
    given:
    List<Date> dates = ['2015/5/5', '2015/1/1', '2015/10/10', '2015/11/3'].memberAs(Date)

    when:
    def firstDate = FirstDate.firstDate(dates)

    then:
    firstDate == '2015/1/1' as Date
}
```

見た目的には完璧ですが、Intellij IDEA さんが、「List には memberAs なんてないよ？」という警告を出しやがります。また、dates に型の宣言 `List<Date>` が必要になります。
ここを `def` にすると「dates の型が特的できないよ？」という警告を出しやがります。

このテストでは問題がありませんが、Spock のデータテーブルを使いたい場合や、メソッドに直接渡したい場合など、型宣言ができない場合があります。そうすると `dates` に関連する呼び出しで芋づる式に型推論が効かなくなって使い勝手が落ちてしまう場合があります。

# 現時点での自分の採用している方法

`List<Date>` が沢山必要なプログラムのテストをしているので、`collect asDate` による方法（collectの変換クロージャを外出し）を採用しています（実際のプロダクトでは Date じゃないクラスです）。

Groovy は Gradle と Spock にしか使ってない弱者なので、強い人が採用している方法を知りたいと思って記事を書いてみました。