spock での全テストがとても遅いプロジェクトがあってちょっと困っている。
べき論はテストの再設計なんだけど、取り急ぎ日に何十回も実行するテストと、日に一回実行するテストに分けようと思う。
そのために、phpunit にあったグルーピングの機能が spock にも欲しかった。
(@group wip
ってテストに付与しておいて、実行時に--group=wip
って指定するとそのグループだけ実行される機能)
無いことはないだろうと思って調べてみたものの、どうにも spock 標準には直接その様な機能はないみたい。
けど色々見ていたら junit の機能を使って実現できそうなことが分かったので、まてめておく。
コード
この記事の成果物は以下で公開しています。
https://github.com/suzuki-hoge/category-test
リンク先の README および公開物で分かるよって人は、この記事はここまででほとんどお終いです。
ここから先はやってみようとしたときにちょっとアレンジとかが出来る様に、詳細な方法を記述します。
準備
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
}
}
$ 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
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
ではない)
public @interface Category {
Class<?>[] value();
}
どうやら任意数のClass
を指定すれば良いので、超適当にjava.lang.String
でも突っ込んでみる。
import org.junit.experimental.categories.Category
@Category(String)
class CommonApiTest extends Specification { ... }
そしてテスト設定の中に以下のuseJUnit
の記載を追加する。
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
を用意した。
package category
@interface Common { }
付与し直す。
+ import category.Common
import org.junit.experimental.categories.Category
- @Category(String)
+ @Category(Common)
class CommonApiTest extends Specification {
以下のふたつは複数付与をする。
+ @Category([Payment, DatabaseMock])
class PaymentSystemTest extends Specification {
+ @Category([Video, DatabaseMock])
class VideoSystemTest extends Specification {
テスト設定を修正する。
includeCategories
は可変長引数なので、複数指定することが可能な様だ。
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
に変える。
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
これも素直に想像するとおりの挙動みたいだ。
実行時にパラメータで設定できるようにする
こんなコードを埋めて、プロパティの取れ方を確認する。
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
パラメータをクラス名にする適当なコードを書く
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
および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
期待通りの挙動をしている。
おまけ:継承
良くあるテストの共通化で親クラスがある場合を想定して動作確認をする。
@Category(DatabaseMock)
class DatabaseMockSpecification extends Specification { }
- @Category([Payment, DatabaseMock])
+ @Category(Payment)
- class PaymentSystemTest extends Specification {
+ class PaymentSystemTest extends DatabaseMockSpecification {
- @Category([Video, DatabaseMock])
+ @Category(Video)
- class VideoSystemTest extends Specification {
+ class VideoSystemTest extends DatabaseMockSpecification {
実行してみたところ、何もテストされていない。
$ ./gradlew test --rerun-tasks -Dcategories=db-mock | grep out:
どうやら子供クラス側で@Category
を指定すると、上書きしてしまうみたいだ。
以下の様にしたら動いたけど、複数箇所で指定することはできないのかな?
- @Category(Payment)
class PaymentSystemTest extends DatabaseMockSpecification {
- @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 からの実行
Run
-> Edit Configurations...
にCategory
の項目があった。
複数指定とかパッケージ指定との兼用とかまで確認できてないけど、僕は IntelliJ からテストしないからとりあえずはこれで良しとする。
おまけ:依存関係
CategoryParser
はbuild.gradle
から使いたいからbuildSrc/
に置いたけど、アノテーションはsrc/test/
から使うのでbuildSrc
に置けなかった。
buildSrc
からsrc/test/
に置いてあるアノテーションに依存しているのがとても気持ち悪い(し、アノテーションの rename に追従できない)のが気になっている。
気が向いたらそのうち調べてみるかもしれない。
以上。