groovy spock のテストをグルーピングして部分的に実行したい

spock での全テストがとても遅いプロジェクトがあってちょっと困っている。
べき論はテストの再設計なんだけど、取り急ぎ日に何十回も実行するテストと、日に一回実行するテストに分けようと思う。

そのために、phpunit にあったグルーピングの機能が spock にも欲しかった。
（`@group wip`ってテストに付与しておいて、実行時に`--group=wip`って指定するとそのグループだけ実行される機能）

無いことはないだろうと思って調べてみたものの、どうにも spock 標準には直接その様な機能はないみたい。
けど色々見ていたら junit の機能を使って実現できそうなことが分かったので、まてめておく。

## コード
この記事の成果物は以下で公開しています。
https://github.com/suzuki-hoge/category-test

リンク先の README および公開物で分かるよって人は、この記事はここまででほとんどお終いです。

ここから先はやってみようとしたときにちょっとアレンジとかが出来る様に、詳細な方法を記述します。

## 準備
gradle の標準スタイルでプロジェクトを準備し、適当な構成のテストコード群を作成する。

```groovy:build.gradle
group 'category'
version '1.0-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'groovy'

version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.codehaus.groovy:groovy-all:2.4.1'
    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
}

// どのテストが実行されたか出力するために追記
tasks.withType(Test) {
    testLogging {
        setShowStandardStreams true
    }
}
```

```:tests
$ tree --charset=C src/test/groovy/
.
|-- api
|   |-- common
|   |   `-- CommonApiTest.groovy
|   |-- payment
|   |   `-- PaymentApiTest.groovy
|   `-- video
|       `-- VideoApiTest.groovy
|-- service
|   |-- common
|   |   `-- CommonServiceTest.groovy
|   |-- payment
|   |   `-- PaymentServiceTest.groovy
|   `-- video
|       `-- VideoServiceTest.groovy
`-- system
    |-- common
    |   `-- CommonSystemTest.groovy
    |-- payment
    |   `-- PaymentSystemTest.groovy
    `-- video
        `-- VideoSystemTest.groovy
```

```groovy:CommonApiTest.groovy
class CommonApiTest extends Specification {
    def test() {
        expect:
        println 'out: api common'
        true
    }
}
```

とりあえず通るテストを適当に作り、適当にテストを実行する。

```
$ ./gradlew test --rerun-tasks | grep out: 
    out: api common
    out: api payment
    out: api video
    out: service common
    out: service payment
    out: service video
    out: system common
    out: system payment, system db-mock
    out: system video,   system db-mock
```

`PaymentSystemTest`と`VideoSystemTest`だけは少しだけ出力を他と変えてある。
これで準備完了。

## junit の`@Category`をテストクラスに付与する
開いてみたらこれだけのアノテーションだった。（`groovy.lang.Category`ではない）

```java:org.junit.experimental.categories.Category
public @interface Category {
    Class<?>[] value();
}
```

どうやら任意数の`Class`を指定すれば良いので、超適当に`java.lang.String`でも突っ込んでみる。

```groovy:CommonApiTest.groovy
import org.junit.experimental.categories.Category

@Category(String)
class CommonApiTest extends Specification { ... }
```

そしてテスト設定の中に以下の`useJUnit`の記載を追加する。

```groovy:build.gradle
tasks.withType(Test) {
    useJUnit {
        includeCategories 'java.lang.String'
    }

    ...
}
```

そしてもう一度実行してみる。

```
$ ./gradlew test --rerun-tasks | grep out:
    out: api common
```

`java.lang.String`を`@Category`で指定したクラスだけ実行された。
これを基本に進めることにする。

## `@Category`に設定するグループを準備する
`String`ではいくらなんでもなので、`Common`, `Payment`, `Video`, `DatabaseMock`を用意した。

```groovy:Common.groovy
package category

@interface Common { }
```

付与し直す。

```diff:CommonApiTest.groovy
+ import category.Common
import org.junit.experimental.categories.Category

- @Category(String)
+ @Category(Common)
class CommonApiTest extends Specification { 
```

以下のふたつは複数付与をする。

```diff:PaymentSystemTest.groovy
+ @Category([Payment, DatabaseMock])
class PaymentSystemTest extends Specification {
```

```diff:VideoSystemTest.groovy
+ @Category([Video, DatabaseMock])
class VideoSystemTest extends Specification {
```

テスト設定を修正する。
`includeCategories`は可変長引数なので、複数指定することが可能な様だ。

```diff:build.gradle
tasks.withType(Test) {
    useJUnit {
-         includeCategories 'java.lang.String'
+         includeCategories 'category.Payment', 'category.Video'
    }
}
```

再実行してみる。

```
$ ./gradlew test --rerun-tasks | grep out:
    out: api payment
    out: api video
    out: service payment
    out: service video
    out: system payment, system db-mock
    out: system video,   system db-mock
```

期待通りだ。

## 除外設定の動作確認もしてみる
`include`を`exclude`に変える。

```diff:build.gradle
tasks.withType(Test) {
    useJUnit {
-         includeCategories 'category.Payment', 'category.Video'
+         excludeCategories 'category.Payment', 'category.Video'
    }
}
```

再実行してみる。

```
$ ./gradlew test --rerun-tasks | grep out:
    out: api common
    out: service common
    out: system common
```

これも素直に想像するとおりの挙動みたいだ。

## 実行時にパラメータで設定できるようにする
こんなコードを埋めて、プロパティの取れ方を確認する。

```groovy:build.gradle
println 'prop: ' + System.getProperty("categories")
```

```:単数指定
$ ./gradlew test --rerun-tasks -Dcategories=payment | grep prop:
prop: payment
```

```:複数指定
$ ./gradlew test --rerun-tasks -Dcategories=payment,video | grep prop: 
prop: payment,video
```

```:複数指定（これは当然エラー）
$ ./gradlew test --rerun-tasks -Dcategories=payment, video | grep prop:
prop: payment,

FAILURE: Build failed with an exception.
```

```:複数指定（これなら問題ない）
$ ./gradlew test --rerun-tasks -Dcategories='payment, video' | grep prop:
prop: payment, video
```

```:記号も扱える
$ ./gradlew test --rerun-tasks -Dcategories=db-mock,-video | grep prop:
prop: db-mock,-video
```

```:未指定（値なし）
$ ./gradlew test --rerun-tasks -Dcategories= | grep prop: 
prop: 
```

```:未指定（キーなし）
$ ./gradlew test --rerun-tasks | grep prop: 
prop: null
```

## パラメータをクラス名にする適当なコードを書く
```groovy:build.gradle
CategoryParser cp = new CategoryParser(System.getProperty('categories'))

println "prop: ${cp.includes()}"
```

詳細な実装は冒頭で触れた github で公開しています。

+ [CategoryParser](https://github.com/suzuki-hoge/category-test/blob/master/buildSrc/src/main/groovy/CategoryParser.groovy)
+ [CategoryParserTest](https://github.com/suzuki-hoge/category-test/blob/master/buildSrc/src/test/groovy/CategoryParserTest.groovy)

```
$ ./gradlew test --rerun-tasks -Dcategories=payment,video | grep prop:
prop: [category.Payment, category.Video]
```

ちゃんと指定したパラメータがクラス名になっている。
これを使って動的に設定すれば良いだろう。

```diff
tasks.withType(Test) {
+     CategoryParser cp = new CategoryParser(System.getProperty('categories'))

    useJUnit {
-         excludeCategories 'category.Payment', 'category.Video'
+         includeCategories cp.includes()
+         excludeCategories cp.excludes()
    }
}
```

`CategoryParser`および`CategoryParserTest`は`buildSrc/`配下に置いた。
テストスクリプト等のプロダクトコードに含めないコードはそこに置くと`build.gradle`から見える様になっている。

また、gradle の`test`コマンドには buildSrc の`test`が包含されている。

## 出来たので動作確認をする
```:何も指定しない場合
$ ./gradlew test --rerun-tasks | grep out: 
    out: api common
    out: api payment
    out: api video
    out: service common
    out: service payment
    out: service video
    out: system common
    out: system payment, system db-mock
    out: system video,   system db-mock
```

```:単数指定
$ ./gradlew test --rerun-tasks -Dcategories=video | grep out:
    out: api video
    out: service video
    out: system video,   system db-mock
```

```:複数指定
$ ./gradlew test --rerun-tasks -Dcategories=payment,video | grep out:      
    out: api payment
    out: api video
    out: service payment
    out: service video
    out: system payment, system db-mock
    out: system video,   system db-mock
```

```:除外混合
$ ./gradlew test --rerun-tasks -Dcategories=video,-db-mock | grep out:
    out: api video
    out: service video
```

```:パッケージ指定（標準機能）
$ ./gradlew test --rerun-tasks --tests 'system.*' | grep out:
    out: system common
    out: system payment, system db-mock
    out: system video,   system db-mock
```

```:パッケージ指定+カテゴリ指定
$ ./gradlew test --rerun-tasks --tests 'system.*' -Dcategories=video | grep out:
    out: system video,   system db-mock
```

期待通りの挙動をしている。

## おまけ：継承
良くあるテストの共通化で親クラスがある場合を想定して動作確認をする。

```groovy:DatabaseMockSpecification.groovy
@Category(DatabaseMock)
class DatabaseMockSpecification extends Specification { }
```

```diff:PaymentSystemTest.groovy
- @Category([Payment, DatabaseMock])
+ @Category(Payment)
- class PaymentSystemTest extends Specification {
+ class PaymentSystemTest extends DatabaseMockSpecification {
```

```diff:VideoSystemTest.groovy
- @Category([Video, DatabaseMock])
+ @Category(Video)
- class VideoSystemTest extends Specification {
+ class VideoSystemTest extends DatabaseMockSpecification {
```

実行してみたところ、何もテストされていない。

```
$ ./gradlew test --rerun-tasks -Dcategories=db-mock | grep out:

```

どうやら子供クラス側で`@Category`を指定すると、上書きしてしまうみたいだ。
以下の様にしたら動いたけど、複数箇所で指定することはできないのかな？

```diff:PaymentSystemTest.groovy
- @Category(Payment)
class PaymentSystemTest extends DatabaseMockSpecification {
```

```diff:VideoSystemTest.groovy
- @Category(Video)
class VideoSystemTest extends DatabaseMockSpecification {
```

```
$ ./gradlew test --rerun-tasks -Dcategories=db-mock | grep out:
    out: system payment, system db-mock
    out: system video,   system db-mock
```

とりあえず確認しておいて良かった。

## おまけ：未定義のパラメータ
勘違いや誤用があると大変なので、期待していないパラメータはテスト実行前に落ちるようにしておいた。

```
$ ./gradlew test --rerun-tasks -Dcategories=payment,foo,-video,-bar

FAILURE: Build failed with an exception.
> no such test categories: [foo, bar]
```

## おまけ：IntelliJ からの実行
いつもの全テスト。
<img width="1454" alt="all.png" src="https://qiita-image-store.s3.amazonaws.com/0/113398/5a7a76a1-40ff-643c-f611-79b99d9be1e3.png">

`Run` -> `Edit Configurations...`に`Category`の項目があった。
<img width="1072" alt="conf.png" src="https://qiita-image-store.s3.amazonaws.com/0/113398/91504a93-fa49-85e7-c84b-058bb176129b.png">

適当に打って探す。
<img width="639" alt="select.png" src="https://qiita-image-store.s3.amazonaws.com/0/113398/5d2c2f98-d91f-1482-f8ff-fb414455343d.png">

動作確認できた。
<img width="1454" alt="payment.png" src="https://qiita-image-store.s3.amazonaws.com/0/113398/4069d175-a8e6-73cd-aff1-110a234d693e.png">

複数指定とかパッケージ指定との兼用とかまで確認できてないけど、僕は IntelliJ からテストしないからとりあえずはこれで良しとする。

## おまけ：依存関係
`CategoryParser`は`build.gradle`から使いたいから`buildSrc/`に置いたけど、アノテーションは`src/test/`から使うので`buildSrc`に置けなかった。

`buildSrc`から`src/test/`に置いてあるアノテーションに依存しているのがとても気持ち悪い（し、アノテーションの rename に追従できない）のが気になっている。
気が向いたらそのうち調べてみるかもしれない。

以上。