Spockでユニットテストを書いている、Gradleを使ったJavaプロジェクトがある。
Intellijでユニットテストを実行するとき、ソースコードに変更があると毎回ユニットテストのすべてのクラスがコンパイルされてしまい、実行までに2分前後のコンパイル待ちが発生していた。
Javaのようにインクリメンタルコンパイル(増分コンパイル)ができないかと思って調べたら、設定できたのでメモ。
環境
Gradle 7.1.1、OpenJDK 11にて確認。
Gradle 5.6以降、JDK 7以上で利用可能。
問題
Gradleプロジェクトにて、Spockを使ったGroovyテストクラスに1つでも変更があると、常にすべてのクラスがコンパイルされる。
また、テストクラスに変更がなくても、Javaのメインクラスに変更があった場合も、同様にすべてのクラスがコンパイルされる。
Gradleによるコンパイル時、Javaであればデフォルトでインクリメンタルコンパイル(増分コンパイル・差分コンパイル)が有効になっているため、一度コンパイルした後は、変更のあったクラスおよび変更の影響を受けるクラスだけがコンパイルされる。
Groovyの場合、デフォルトではインクリメンタルコンパイルが有効になっていないため、テストクラスに変更があった場合や、メインクラスが変更された場合、すべてのテストクラスが再コンパイルされていると思われる。
Groovyテストクラス数が多く、フルコンパイルにはおおよそ2分程度かかるため、短縮したい。
対応
インクリメンタルコンパイルをGroovyコンパイルにも適応できるが、デフォルトでは無効になっているため、有効化する。
手順としては以下の2つ。
- コンパイル回避の有効化
- インクリメンタルビルドの有効化
また、これらの変更により、テストクラスのコンパイルが UP-TO-DATE
と判定された場合、デフォルトではユニットテストの実行自体が行われなくなるため、Gradleの test
タスクを常に実行するよう build.gradle
にも変更を行う。
設定手順
Groovyコンパイル回避の有効化
インキュベーション機能のため、明示的に有効化する必要がある。
こちらの手順通り、Javaプロジェクトの settings.gradle
に、以下の記述を追加。
enableFeaturePreview('GROOVY_COMPILATION_AVOIDANCE')
Groovyインクリメンタルビルドの有効化
Groovy Pluginの説明を基に、 build.gradle
に設定を追加。
tasks.withType(GroovyCompile).configureEach {
options.incremental = true
}
GroovyのコンパイルオプションであるgroovyOptionsではなく、Javaのコンパイルオプションである[options]が設定の対象。
手順通り GroovyCompile
タスクすべてに設定しているが、ユニットテストだけが対象であれば、 compileTestGroovy
タスクに対してのみ設定すればいいと思われる。
常にユニットテストを実行するよう設定
先の2つの設定で、Groovyのインクリメンタルコンパイルは有効となるが、 compileTestJava
および compileTestGroovy
タスクが両方とも UP-TO-DATE
になると、 test
タスクも UP-TO-DATE
扱いで実行されなくなる。
これを避けるために、常に test
タスクを実行するよう、 build.gradle
に以下の設定を追加。
test {
outputs.upToDateWhen { false }
}
TaskOutputs#upToDateWhenで false
を返すことで、常に test
タスクを実行できる。
結果
これらの設定を行ったことで、Javaのメインクラスを変更した場合でも、メソッドの内部のみの変更であれば、Groovyテストクラスは UP-TO-DATE
となり、再コンパイルが行われなくなった。
また、テストクラスの一部を変更した場合、差分に対してのみコンパイルが行われるようになったため、1クラスの変更であれば数秒で再コンパイルが完了するようになった。
振り返り
インクリメンタルコンパイルについては、インクリメンタルJavaコンパイルおよびインクリメンタルJavaコンパイルの既知の問題を参照。
有効化したことで劇的な効果があった。Gradle 7.2時点ではデフォルトで有効化されていないので、Spockなどを使っていればメリットがありそう。