LoginSignup
3
4

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-21

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

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

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

コード

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

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

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

準備

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

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
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

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

junit の@Categoryをテストクラスに付与する

開いてみたらこれだけのアノテーションだった。(groovy.lang.Categoryではない)

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

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

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

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

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

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を用意した。

Common.groovy
package category

@interface Common { }

付与し直す。

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

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

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

PaymentSystemTest.groovy
+ @Category([Payment, DatabaseMock])
class PaymentSystemTest extends Specification {
VideoSystemTest.groovy
+ @Category([Video, DatabaseMock])
class VideoSystemTest extends Specification {

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

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

期待通りだ。

除外設定の動作確認もしてみる

includeexcludeに変える。

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

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

実行時にパラメータで設定できるようにする

こんなコードを埋めて、プロパティの取れ方を確認する。

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

パラメータをクラス名にする適当なコードを書く

build.gradle
CategoryParser cp = new CategoryParser(System.getProperty('categories'))

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

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

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

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

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

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

CategoryParserおよびCategoryParserTestbuildSrc/配下に置いた。
テストスクリプト等のプロダクトコードに含めないコードはそこに置くと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

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

おまけ:継承

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

DatabaseMockSpecification.groovy
@Category(DatabaseMock)
class DatabaseMockSpecification extends Specification { }
PaymentSystemTest.groovy
- @Category([Payment, DatabaseMock])
+ @Category(Payment)
- class PaymentSystemTest extends Specification {
+ class PaymentSystemTest extends DatabaseMockSpecification {
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を指定すると、上書きしてしまうみたいだ。
以下の様にしたら動いたけど、複数箇所で指定することはできないのかな?

PaymentSystemTest.groovy
- @Category(Payment)
class PaymentSystemTest extends DatabaseMockSpecification {
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 からの実行

いつもの全テスト。
all.png

Run -> Edit Configurations...Categoryの項目があった。
conf.png

適当に打って探す。
select.png

動作確認できた。
payment.png

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

おまけ:依存関係

CategoryParserbuild.gradleから使いたいからbuildSrc/に置いたけど、アノテーションはsrc/test/から使うのでbuildSrcに置けなかった。

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

以上。

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4