I’am currently working on a project where we try to move an ant based build to gradle. One task is to make the tests build and run from gradle. The build creates multiple artifacts and there are test dependencies between the sub-projects which gradle does not handle out of the box.
Let’s say we have a multi-project build with Project B depending on Project A. B does not only have a compile dependency on A but also a test dependency. The tests in B depend on a couple of test helper classes from A.
Handling this with gradle was not as straight forward as I had hoped. In the end it wasn’t very difficult but it took me some effort to understand the details. This may be completely obvious to you. :-)
There are a couple of possibilities:
the naive approach
The dependency for building of B is easy:
build.gradle:
dependencies {
compile project (':A')
}
This will add the jar artifact from A as a dependency to project B. We can confirm this by using the following snippet to print the compile configuration:
configurations.compile.each {
println "compile: $it"
}
which will print:
compile: <path>/ProjectA/build/libs/ProjectA.jar
So the naive approach is to change the dependencies to:
build.gradle:
dependencies {
compile project (':A')
testCompile project (':A')
}
But this does not work. Printing testCompile
with:
configurations.testCompile.each {
println "testCompile: $it"
}
still prints:
testCompile: <path>/ProjectA/build/libs/ProjectA.jar
...
only. Adding testCompile
didn’t add anything to the dependencies. When we print the testCompile
dependencies without the testCompile
line we get the same output. testCompile
extends compile
and it doesn’t add an artifact we could depend on.
I found two solutions on stackoverflow.
a simple solution, depending on the test output paths
The simpler solution adds a dependency on the test sourceset output:
dependencies {
...
testCompile project(':A').sourceSets.test.output
}
Ok ….? What’s this?
We use sourceSets
to group source files. In this case we depend on the output
of the test
sources group (by default: src/test/java
and src/test/resources
). The output
contains the path to the compiled test sources and resource files.
Let’s see what we get printed for testCompile
now:
testCompile: <path>/ProjectA/build/classes/test
testCompile: <path>/ProjectA/build/resources/test
testCompile: <path>/ProjectA/build/libs/LearnA.jar
Looks good and it works. :-)
What I dislike about this solution is that we explicitly depend on some internals from B. It would be nice if we could hide this detail from A. Another issue is handling transitive test dependencies of A. We do not want to handle that in B. When we depend on the test code from A, A should take care of its transitive test dependencies as it does for a normal jar artifact.
improved solution, using a configuration
We can hide the internals of A by creating a new configuration
in A …
build.gradle in A:
configurations {
testOutput
}
dependencies {
testOutput sourceSets.test.output
}
… and adding the dependency on this configuration in B:
build.gradle in B:
dependencies {
...
testCompile project(path: ':A', configuration: 'testOutput')
}
A configuration is simply a group of dependencies. We create a new one using the configurations
object and can then add dependencies to it. To depend on it in B we pass a configuration
parameter to the project
method.
I like this one because using the testOutput
configuration we have created something like an interface to the test output. The configuration is also used in the next solution.
improved solution, depending on a test jar
The second solution from stackoverflow creates a test jar from the test sources and uses a configuration
to handle the dependencies.
build.gradle in A:
task jarTest (type: Jar) {
from sourceSets.test.output
classifier = 'test'
}
configurations {
testOutput
}
artifacts {
testOutput jarTest
}
The jarTest
task builds the jar from
the test outputs adding a ‘test’ to the jar name (there are more properties to customize the jar name). Then we create the configuration
. We have seen this in the previous solution. Last step is to add the jar artifact
to the configuration
.
When someone asks for testOutput
gradle will run the jarTest
task.
The dependency configuration in B does not change:
build.gradle in B:
dependencies {
...
testCompile project(path: ':A', configuration: 'testOutput')
}
When we now look at the testCompile
output from our snippet we see the production and the test jar:
testCompile: <path>/ProjectA/build/libs/ProjectA.jar
testCompile: <path>/ProjectA/build/libs/ProjectA-test.jar
improving jarTest
Although gradle seems to always build the test stuff before creating the jar (the from
triggers building the test code?) there is no explicit dependency on a test build task. Looking at the task dependencies of the java plugin jar
depends on classes
, so it is probably a good idea to add a depends on on testClasses
to the jarTest
:
task jarTest (type: Jar, dependsOn: testClasses) {
from sourceSets.test.output
classifier = 'test'
}
handling transitive dependencies
Next let us assume that ProjectA-test.jar
depends on another project: T. With the current solution running the tests of B will fail because the testRuntime classpath won’t contain T.
First we need T to build A. That’s easy we just add a testCompile
dependency:
dependencies {
testCompile project (':T')
}
But this doesn’t help at runtime, B depends on the testOutput
configuration which doesn’t know anything about T. So let’s add the dependency on T:
dependencies {
testCompile project (':T')
testOutput project (':T')
}
testOutput
is a dependency configuration like testCompile
so we can add our dependency on T as we did on testCompile
.
Printing testRuntime
when we run B
configurations.testRuntime.each {
println "testRuntime: $it"
}
we can check that it contains:
testRuntime: <path>/ProjectT/build/libs/ProjectT.jar
.. and it doesn’t fail anymore. Nice :-)
cleaning up duplication
There is just one small issue left: by adding the T dependency to testCompile
and testOutput
we have created a little bit of duplication.
Since testOutput
depends on building the test code it would be helpful to tell gradle that testOutput
should re-use the configuration from testCompile
. Then we wouldn’t need to explicitly list the T dependency on testOutput
.
We can by defining testOutput
like this:
configurations {
testOutput.extendsFrom (testCompile)
}
We can remove the explicit T dependency on testOutput
(removing the duplication) now because we will get it from testCompile
.
final solution
Our final solution needs more code than just adding a dependency on the test output paths but it will make it easier to
handle the test dependencies and their transitive dependencies in a multi-project build.
Here is the final code:
build.gradle in A:
configurations {
testOutput.extendsFrom (testCompile)
}
dependencies {
testCompile project (':T')
}
task jarTest (type: Jar, dependsOn: testClasses) {
from sourceSets.test.output
classifier = 'test'
}
artifacts {
testOutput jarTest
}
build.gradle in B:
dependencies {
...
testCompile project(path: ':A', configuration: 'testOutput')
}
conclusion
After playing around with this for a while it doesn’t look as magic as at first sight but there are still a few questions…
Gradle is really simple for builds that don’t need anything special but it is not so simple anymore if you want to do something a little bit different. Interesting is that the final solution does not have much code. That’s nice but it is still not very obious to implement it like this (.. at least to me).
My next step is to make a plugin from it so we do not have to duplicate that code into all our sub-projects :-)
Update: Nov 2015
Working code is available from my github page (it is slightly different than the version presented above, which may be the reason for the problems mentioned in the comments). It is a gradle project that creates a gradle plugin. I have also created a simple example project that I have run with gradle 2.2 up to 2.8 without problem. Hope that helps!
Update: Dec 2015
This is now available as a plugin from plugins.gradle.org.
Hi, what’s the advantage of using the configuration with a jar instead of without a jar?
It is easier and cleaner to add a jar as dependency instead of directly accessing the test class files in another project.
I was stuck in the same problem. Finally got it resolved. Thanks for posting this. Had asked a similar question here:
https://stackoverflow.com/questions/47610056/unable-to-add-dependency-module-in-gradle-intellij/47627780#47627780
Many thanks – saved me a lot of time and stress!
Thanks for this blog post and thanks for publishing the plugin. Helps me to cope with the current project structure while migrating from Maven to Gradle.
Ok, still using something lower than 2.6 here. I will take a look what the problem is with the newer versions.
Not sure what the problem is. I have created a demo project that runs with gradle 2.8. I noticed that the code I use is not exactly the same as in the article. See my Nov 2015 update at the end of the article.
build.gradle in B:
dependencies {
…
testCompile project(path: ‘:A’)
}
on gradle 2.14.1, that’s all it requires.
doesn’t work for gradle 2.8
No longer works… blows up at: sourceSets.test
It is still working here, which gradle version?
Seems sourceSets.test does’t work on gradle 2.6.