はじめに
Gradleで複数プロジェクトを管理していると、しばしば共通の依存を集約して書きたいシーンがあります。
その中でも今回は、テスト向けの依存を共通化する方法について書きます。特にテスト向けに絞る必要はないのですが、テストに焦点をあてることでcompileClasspath
/testCompileClasspath
あたりの理解も深まったので、そのような内容としました。
プロジェクト構成とゴール
今回は次のような構成でプロジェクトを作成します。

まず上図の通りにプロジェクト間の依存だけを宣言し、:test-base
にはテスト用の依存としてorg.opentest4j:opentest4j
のみを定義してスタートします(opentest4j
を選んだのはdependency treeをシンプルにするためです)。
- app1とapp2の
build.gradle.kts
plugins {
`java`
}
dependencies {
implementation(projects.appBase)
}
- app-baseの
build.gradle.kts
plugins {
`java-library`
}
dependencies {
implementation(projects.testBase)
}
- test-baseの
build.gradle.kts
plugins {
`java-library`
}
dependencies {
implementation("org.opentest4j:opentest4j:1.3.0")
}
そしてゴールは、:test-base
にまとめて定義される依存を、:app1
、:app2
にまで適用することです。
今回はappが2つですが、実際にはもっと増えるイメージなので、各appから直接testImplementation(:test-base)
せずにそれを達成したいとします。
implementation
とapi
を理解する
最初に現状を確認します。各subprojectの依存、compileClasspath
とtestCompileClasspath
をproject-reportプラグインで可視化します。
$ ./gradlew dependencyReport
------------------------------------------------------------
Project ':app1'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
\--- project :app-base
testCompileClasspath - Compile classpath for source set 'test'.
\--- project :app-base
------------------------------------------------------------
Project ':app-base'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
\--- project :test-base
testCompileClasspath - Compile classpath for source set 'test'.
\--- project :test-base
------------------------------------------------------------
Project ':test-base'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
\--- org.opentest4j:opentest4j:1.3.0
testCompileClasspath - Compile classpath for source set 'test'.
\--- org.opentest4j:opentest4j:1.3.0
このようにまず、implementation
で宣言した依存はcompileClasspath
とtestCompileClasspath
に入っていることがわかります。
このままでは:test-base
の依存が:app1
と:app2
にまで伝播することはありません。
では次に、各appに:test-base
の依存を伝播させるために、:app-base
と:test-base
で宣言している依存をapi
に変更してみます。
- app-baseの
build.gradle.kts
dependencies {
- implementation(projects.testBase)
+ api(projects.testBase)
}
- test-baseの
build.gradle.kts
dependencies {
- implementation("org.opentest4j:opentest4j:1.3.0")
+ api("org.opentest4j:opentest4j:1.3.0")
}
再度依存を確認すると、:app1
と:app2
にも:test-base
の依存が入ってきました!🎉
------------------------------------------------------------
Project ':app1'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
\--- project :app-base
\--- project :test-base
\--- org.opentest4j:opentest4j:1.3.0
testCompileClasspath - Compile classpath for source set 'test'.
\--- project :app-base
\--- project :test-base
\--- org.opentest4j:opentest4j:1.3.0
このようにapi
を使うことで利用側にもその依存を伝播させることができます。ポイントとして、api
は利用側のさらに利用側にまで連鎖的に依存が伝播することはありません。あくまで1つ上位の利用側のみに公開されるのみです。
さて、これでテスト向けの依存が各appでも利用可能となったわけですが、問題が1つあります。それは、テスト向けの依存なのにappのcompileClasspath
にも入ってしまっていることです。
これを解決するために、各appで宣言している:app-base
への依存をtestImplementation
に変更します。
dependencies {
- implementation(projects.appBase)
+ testImplementation(projects.appBase)
}
------------------------------------------------------------
Project ':app1'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
No dependencies
testCompileClasspath - Compile classpath for source set 'test'.
\--- project :app-base
\--- project :test-base
\--- org.opentest4j:opentest4j:1.3.0
testImplementation
を使うことでtestCompileClasspath
にのみ依存を入れることができました。
しかしこれだと今度は、:app-base
に集約されるであろう、appのmain sourceのコンパイルに必要な依存やビジネスロジックなどが利用できなくなります。真にやりたいのは:test-base
の依存だけをtestCompileClasspath
に入れることなのです。
build.gradle.kts
で共通の依存に定義する
解決策: ルートの今回のプロジェクト構成では、appが共通に持つ:app-base
という中間のsubprojectを利用してどうにか:test-base
への依存を共通化できないか模索しましたが難しそうに見えます。
しかし今回の場合は、とりあえず全subprojectに:test-base
の依存が入ればよいだけなので、次のような設定で解決できます。
- ルートの
build.gradle.kts
plugins {
...
+ `java`
}
subprojects {
...
+ apply(plugin = "java")
+
+ dependencies {
+ testImplementation(project(":test-base"))
+ }
}
- app1, app2の
build.gradle.kts
dependencies {
- testImplementation(projects.appBase)
+ implementation(projects.appBase)
}
- app-baseの
build.gradle.kts
dependencies {
- api(projects.testBase)
}
最終的には下記のように、:app-base
の依存はcompileClasspath
に残しつつも、:test-base
の依存のみtestCompileClasspath
に残すことが出来ました。
------------------------------------------------------------
Project ':app1'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
\--- project :app-base
testCompileClasspath - Compile classpath for source set 'test'.
+--- project :app-base
\--- project :test-base
\--- org.opentest4j:opentest4j:1.3.0
ちなみに今回は簡単のために全subprojectに一斉適用していますが、例えばappだけに適用したいという場合も convention plugin 等を使えば可能です(この辺りは別途書こうと思っているのでここでは割愛)。
参考: Sharing build logic between subprojects Sample
別解: testFixturesを使う
java-test-fixtures というプラグインを導入すると、testFixturesCompileClasspath
というclasspathが追加され、そこの依存を利用側ではtestImplementation(testFixtures(:app-base)
のようにして引き継ぐことができます。
これまでのimplementaion(:app-base)
やtestImplemenntation(:app-base)
では、:app-base
自体とapi
で定義されたcompileClasspath
上のクラスしか引き継ぐことが出来ませんでしたが、testFixtures(:app-base)
とすることでtestFixturesCompileClasspath
上のクラスを引き継ぐことができるようになります。
なのでこんな事ができます。まずルートのbuild.gradle.kts
でjava-test-fixtures
プラグインを適用します。
plugins {
...
+ `java-test-fixtures`
}
subprojects {
+ apply(plugin = "java-test-fixtures")
...
}
:test-base
の持つ依存はapi("org.opentest4j:opentest4j:1.3.0")
のままで、依存だけ確認するとtestFixturesCompileClasspath
にも追加されていることがわかります。
------------------------------------------------------------
Project ':test-base'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
\--- org.opentest4j:opentest4j:1.3.0
testCompileClasspath - Compile classpath for source set 'test'.
+--- org.opentest4j:opentest4j:1.3.0
+--- project :test-base (*)
\--- project :test-base (*)
testFixturesCompileClasspath - Compile classpath for source set 'test fixtures'.
+--- project :test-base (*)
\--- org.opentest4j:opentest4j:1.3.0
次に:app-base
のbuild.gradle.kts
では:test-base
のtestFixtures
をapi
で引き継ぎます。
dependencies {
+ testFixturesApi(testFixtures(projects.testBase))
}
すると:test-base
の依存はtestFixturesCompileClasspath
にだけ放り込むことができます。
------------------------------------------------------------
Project ':app-base'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
No dependencies
testCompileClasspath - Compile classpath for source set 'test'.
+--- project :app-base (*)
+--- project :app-base (*)
\--- project :test-base
+--- project :test-base (*)
\--- org.opentest4j:opentest4j:1.3.0
testFixturesCompileClasspath - Compile classpath for source set 'test fixtures'.
+--- project :app-base (*)
\--- project :test-base
+--- project :test-base (*)
\--- org.opentest4j:opentest4j:1.3.0
最後に、各appで:app-base
のtestFixtures
だけを引き継ぎます。
dependencies {
implementation(projects.appBase)
+ testImplementation(testFixtures(projects.appBase))
}
そうすると:test-base
の依存が:app-base
を超えて各appのtestCompileClasspath
にだけ入ってきました。
------------------------------------------------------------
Project ':app1'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
\--- project :app-base
testCompileClasspath - Compile classpath for source set 'test'.
+--- project :app-base
| +--- project :app-base (*)
| \--- project :test-base
| +--- project :test-base (*)
| \--- org.opentest4j:opentest4j:1.3.0
+--- project :app1 (*)
+--- project :app-base (*)
\--- project :app1 (*)
testFixturesCompileClasspath - Compile classpath for source set 'test fixtures'.
\--- project :app1 (*)
とはいえ、この方法だと構造がややこしい上に、結局各appでtestImplementation(testFixtures(projects.appBase))
を追加するなら、最初からtestImplementation(projects.testBase)
すれば良いだけ…となるのでちょっと微妙ですね。
ただ、classpathの関係を理解するのには良い題材かなという感じがしました。
余談: testApiについて
超余談ですが、kotlin pluginを使うと testApi
というAPI(configuration)が使えます。testApi(:app-base)
のように使います。
これ、いかにも:app-base
の依存をtestCompileClasspath
に入れつつ、:app-base
の利用側でもtestCompileClasspath
を引き継げるようなAPIに見えるのですが、ここまでの説明で分かる通り、testImplementation(:app-base)
と書いても引き継げるのはcompileClasspath
で公開されているクラスだけなので、そういうことはできません。
意味不明だなと思って調べていたら、同じことを言っている人がいて少し安心しました。そうそう、test dependenciesに関してapiって関係がないんですよね。
I’m not sure why this was added, but as best I can tell it increases complexity without much benefit.
For one: test source set code generally isn’t depended upon. Gradle offers Test Fixtures for that. If the idiomatic purpose for Gradle api configurations is to signify dependencies that are part of an outgoing variant’s public ABI, it’s not really relevant for test dependencies.
— KGP: Deprecate and remove `testApi` configuration : KT-63285
もしかするとtestFixtures
のようにtestCompileClasspath
を引き継ぐ方法もあるのかもしれませんが、ややこしいので使う必要はなさそうです。
結論
- 依存は、ルートの
build.gradle.kts
や convention plugin を使って共通化できる。 implementaion
やapi
を使った依存はcompileClasspath
とtestCompileClasspath
の両方に入る。一方testImlementaion
の依存はtestCompileClasspath
にしか入らない。api
で宣言した依存は利用側にも公開され伝播する。ただし、連鎖的に伝播することはない。testApi
というAPIが存在するが、そもそもtestCompileClasspath
の依存は利用側に公開されないので使い道がない。testFixtures
の仕組みを使うとtestFixturesCompileClasspath
にテスト向けの依存を入れ伝播させていくことが可能だが、やや複雑である。