Multi-project test dependencies with gradle
Asked Answered
C

17

216

I have a multi-project configuration and I want to use gradle.

My projects are like this:

  • Project A

    • -> src/main/java
    • -> src/test/java
  • Project B

    • -> src/main/java (depends on src/main/java on Project A)
    • -> src/test/java (depends on src/test/java on Project A)

My Project B build.gradle file is like this:

apply plugin: 'java'
dependencies {
  compile project(':ProjectA')
}

The task compileJava work great but the compileTestJava does not compile the test file from Project A.

Confederation answered 13/4, 2011 at 3:17 Comment(1)
possible duplicate: https://mcmap.net/q/48627/-gradle-test-dependencyHiggins
R
156

Deprecated - For Gradle 5.6 and above use this answer.

In Project B, you just need to add a testCompile dependency:

dependencies {
  ...
  testCompile project(':A').sourceSets.test.output
}

Tested with Gradle 1.7.

Retentivity answered 1/11, 2011 at 16:7 Comment(11)
Turns out the classes property is deprecated -- use output instead.Retentivity
This does not work in Gradle 1.3 since sourceSets is no longer a public property of a project.Bateau
Keep in mind the above solution requires at least a gradle testClasses before the build structure is actually valid. E.g. the Eclipse plugin won't let you import the project before that. It really is a shame testCompile project(':A') does not work. @DavidPärsson: "Gradle 1.3" contradicts "no longer" since Fesler tested with Gradle 1.7.Loney
How can I prevent that every time the jar builds the tests of it are also executed? That makes it not really useful...Mumble
didn't work for me. Failed with circular dependency: compileTestJava \--- :testClasses \--- :compileTestJava (*)Diarist
Causes errors after including Eclipse project generated with eclipse task.Melliemelliferous
It seems the dependencies are not transitive. With the graph A -> B -> C, C has to add the test output from both A and B.Takin
Don't do this, projects are not supposed to reach into other projects. Instead use Nikita's answer, correctly modelling this as a project dependency.Lermontov
Tested successfully with Gradle 7.4.2Dvorak
This was what I needed for my legacy code even though now we use a higher version of Gradle.Enthusiast
Didn't work for me in kts. testImplementation(project(":graph-rewrite").sourceSets.test.output) says "<html>Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:<br/>public val Project.sourceSets: SourceSetContainer defined in org.gradle.kotlin.dsl<br/>public val KotlinJvmProjectExtension.sourceSets: NamedDomainObjectContainer&lt;KotlinSourceSet&gt; defined in org.gradle.kotlin.dsl" It did work in groovy, but gives a warning "Unrecognized dependency notation" on sourceSetsSchnabel
S
113

This is now supported as a first class feature in Gradle. Modules with java or java-library plugins can also include a java-test-fixtures plugin which exposes helper classes and resources to be consumed with testFixtures helper. Benefit of this approach against artifacts and classifiers are:

  • proper dependency management (implementation/api)
  • nice separation from test code (separate source set)
  • no need to filter out test classes to expose only utilities
  • maintained by Gradle

Example

Only relevant lines shown here, see GitHub for the full compiling example.

:modul:one

modul/one/build.gradle

plugins {
  id("java-library") // or "java"
  id("java-test-fixtures")
}

modul/one/src/testFixtures/java/com/example/Helper.java

package com.example;
public class Helper {}

:modul:other

modul/other/build.gradle

plugins {
  id("java") // or "java-library"
}
dependencies {
  testImplementation(testFixtures(project(":modul:one")))
}

modul/other/src/test/java/com/example/other/SomeTest.java

package com.example.other;
import com.example.Helper;
public class SomeTest {
  @Test public void f() {
    new Helper(); // used from :modul:one's testFixtures
  }
}

Further reading

Sturgis answered 9/2, 2020 at 15:27 Comment(10)
They are working on supporting this on Android, see issuetracker.google.com/issues/139762443 and issuetracker.google.com/issues/139438142Davy
As of now doesn't work for android gradle plugin and kotlinAmundson
Yes, @ArpitA, see the comment before yours. This Q&A is for standard Java Gradle projects.Sturgis
As of now this does work for Kotlin (tested with an internal module on Kotlin 1.6.10)Prosector
testFixtures dependency could be written as testFixturesImplementation in case of sharing "java-test-fixtures" plugin. See docs.gradle.org/current/userguide/…Schweiker
@GrigoryKislin that would mean something else, you would depend on test fixtures of another module from test fixtures. Which, incidentally, might be visible to the test code of the module too. It depends on what you want to expose for users of your text fixtures.Sturgis
This didn't work for me in Kotlin 1.8 I have in both projects id("java-library") id("java-test-fixtures") and in the other project testImplementation(testFixtures(project(":my-project"))) but I still get "unresolved reference" in compilationSchnabel
@Schnabel if the unresolved reference is testFixtures: are you inside a dependencies block? we're supposed to be calling org.gradle.api.artifacts.dsl.DependencyHandler#testFixtures(java.lang.Object) if the unresolved reference is testImplementation: are do you have a Java plugin (one of java/java-library/java-application) applied directly or transitively, and are you inside the dependencies block? If neither, please Google the error as it's unlikely to be related.Sturgis
@Sturgis - the "unresolved reference" is textFixtures. I'm inside the dependencies block.Schnabel
@Schnabel are you maybe using the dependencies block inside kotlin { }? If not, could you try to reproduce with a pull request to the example?Sturgis
S
70

Simple way is to add explicit task dependency in ProjectB:

compileTestJava.dependsOn tasks.getByPath(':ProjectA:testClasses')

Difficult (but more clear) way is to create additional artifact configuration for ProjectA:

task myTestsJar(type: Jar) { 
  // pack whatever you need...
}

configurations {
  testArtifacts
}

artifacts {
   testArtifacts myTestsJar
}

and add the testCompile dependency for ProjectB

apply plugin: 'java'
dependencies {
  compile project(':ProjectA')
  testCompile project(path: ':ProjectA', configuration: 'testArtifacts')
}
Satyr answered 13/4, 2011 at 11:40 Comment(9)
I tried this (the simple way) and while that makes sure it builds the testClasses, it doesn't add the test path to the CLASSPATH so my ProjectB tests that depend on ProjectA test classes still fail to build.Jennifer
The difficult way doesn't work for me, using Gradle 1.8: No signature of method: org.gradle.api.internal.artifacts.dsl.DefaultArtifactHandler.testArtifacts() is applicable for argument types: (org.gradle.api.tasks.bundling.Jar_Decorated) values: [task ':projectA:myTestsJar']. The error is reported for the line "testArtifacts myTestsJar".Halliday
@Halliday you have to add testArtifacts configuration like this: configurations { testArtifacts } for more details see this section of Gradle help: gradle.org/docs/current/dsl/…Satyr
@NikitaSkvortsov thanks that works. I took the freedom to edit your original answer (and upvote it).Halliday
In Gradle 1.8 you may want from sourceSets.test.output and possibly classifier = 'tests' in place of // pack whatever you need... in the answerCampestral
Confirmed that with Gradle 1.12 using the full solution, with @PeterLamberg suggested additions works as expected. Does not impact import of project into Eclipse.Tasteful
The JAR one is the only one that worked for me for an Android test setup, with task jarTest (type: Jar) { from sourceSets.test.output classifier = 'test' } For Android we just have to use "androidTestCompile" instead of "testCompile".Snakebird
In order for the simple way to work under Gradle 2.4 I also had to use Fesler's solution: testCompile project(':A').sourceSets.test.outputDevaluation
This works for me in Gradle 4.7. They now have some docs about the approach at docs.gradle.org/current/dsl/…Oligarch
P
24

I've come across this problem myself recently, and man is this a tough issues to find answers for.

The mistake you are making is thinking that a project should export its test elements in the same way that it exports its primary artifacts and dependencies.

What I had a lot more success with personally was making a new project in Gradle. In your example, I would name it

Project A_Test -> src/main/java

I would put into the src/main/java the files that you currently have in Project A/src/test/java. Make any testCompile dependencies of your Project A compile dependencies of Project A_Test.

Then make Project A_Test a testCompile dependency of Project B.

It's not logical when you come at it from the perspective of the author of both projects, but I think it makes a lot of sense when you think about projects like junit and scalatest (and others. Even though those frameworks are testing-related, they are not considered part of the "test" targets within their own frameworks - they produce primary artifacts that other projects just happen to use within their test configuration. You just want to follow that same pattern.

Trying to do the other answers listed here did not work for me personally (using Gradle 1.9), but I've found that the pattern I describe here is a cleaner solution anyway.

Pearsall answered 31/1, 2014 at 16:13 Comment(3)
Yes, opted for this approach at the end of the day.Rainer
This is the best approach! Except I would keep the test code in project A and move only dependencies for both A src/test/java and B src/test/java to A_Test. Then make Project A_Test a testImplementation dependency of both A and B.Chitchat
Yes, simplest solution. Work with testImplementation dependency in B project.Schweiker
P
22

I know it's an old question but I just had the same problem and spent some time figuring out what is going on. I'm using Gradle 1.9. All changes should be in ProjectB's build.gradle

To use test classes from ProjectA in tests of ProjectB:

testCompile files(project(':ProjectA').sourceSets.test.output.classesDir)

To make sure that sourceSets property is available for ProjectA:

evaluationDependsOn(':ProjectA')

To make sure test classes from ProjectA are actually there, when you compile ProjectB:

compileTestJava.dependsOn tasks.getByPath(':ProjectA:testClasses')
Pul answered 5/2, 2014 at 14:47 Comment(1)
This also worked for me except I had to omit the .classesDir.Rior
J
15

Please read the update bellow.

Similar problems described by JustACluelessNewbie occurs in IntelliJ IDEA. Problem is that dependency testCompile project(':core').sourceSets.test.output actually means: "depend on classes generated by gradle build task". So if you open clean project where classes are not generated yet IDEA won't recognise them and reports error.

To fix this problem you have to add a dependency on test source files next to dependency on compiled classes.

// First dependency is for IDEA
testCompileOnly files { project(':core').sourceSets.test.java.srcDirs }
// Second is for Gradle
testCompile project(':core').sourceSets.test.output

You can observe dependencies recognised by IDEA in Module Settings -> Dependencies (test scope).

Btw. this is not nice solution so refactoring is worth considering. Gradle itself does have special subproject containing test-support classes only. See https://docs.gradle.org/current/userguide/test_kit.html

Update 2016-06-05 More I am thinking about proposed solution less I like it. There are few problems with it:

  1. It creates two dependencies in IDEA. One points to test sources another to compiled classes. And it is crucial in which order these dependencies are recognised by IDEA. You can play with it by changing dependency order in Module settings -> Dependencies tab.
  2. By declaring these dependencies you are unnecessarily polluting dependency structure.

So what's the better solution? In my opinion it's creating new custom source set and putting shared classes into it. Actually authors of Gradle project did it by creating testFixtures source set.

To do it you just have to:

  1. Create source set and add necessary configurations. Check this script plugin used in Gradle project: https://github.com/gradle/gradle/blob/v4.0.0/gradle/testFixtures.gradle (or more recent 8.2.1 version)

  2. Declare proper dependency in dependent project:

    dependencies {
        testCompile project(path: ':module-with-shared-classes', configuration: 'testFixturesUsageCompile')
    }
    
  3. Import Gradle project to IDEA and use the "create separate module per source set" option while importing.

Johnathon answered 24/5, 2016 at 20:50 Comment(3)
@jannis fixed. Btw. Gradle moved its Groovy based test fixtures plugin to new Kotlin based: github.com/gradle/gradle/blob/v5.0.0/buildSrc/subprojects/…Duero
Gradle have now moved this again to github.com/gradle/gradle/blob/v8.2.1/build-logic/…Robinet
For Android developers, there appears to be a blocker to having java-test-fixtures work with the Android Gradle Plugin. See youtrack.jetbrains.com/issue/KT-50667/…Robinet
U
14

Here if you are using Kotlin DSL, you should create your task like that according to Gradle documentation.

Like some previous answer, you need to create a special configuration inside the project that will share its tests class, so that you don't mix test and main classes.

Simple steps

  1. In project A you would need to add in your build.gradle.kts :
configurations {
    create("test")
}

tasks.register<Jar>("testArchive") {
    archiveBaseName.set("ProjectA-test")
    from(project.the<SourceSetContainer>()["test"].output)
}

artifacts {
    add("test", tasks["testArchive"])
}
  1. Then in your project B in the dependencies, you will need to add in your build.gradle.kts:
dependencies {
    implementation(project(":ProjectA"))
    testImplementation(project(":ProjectA", "test"))
}
Usn answered 8/5, 2020 at 15:16 Comment(2)
As written above below another answer: just figured that yes, the configuration should really be named test, and nothing else (otherwise IDEs might have difficulties to pick up the dependency to the test sources).Broadloom
I hate it took me so long to find the kotlin dsl example when googling, thank youAverell
L
12

New testJar based (trnsitive dependancies supported) solution available as gradle plugin:

https://github.com/hauner/gradle-plugins/tree/master/jartest

https://plugins.gradle.org/plugin/com.github.hauner.jarTest/1.0

From documentation

In case you have a multi-project gradle build you may have test dependencies between sub-projects (which probably is a hint that your projects are not well structured).

For example assume a project where the sub-project Project B depends on Project A and B does not only have a compile dependency on A but also a test dependency. To compile and run the tests of B we need some test helper classes from A.

By default gradle does not create a jar artifact from the test build output of a project.

This plugin adds a testArchives configuration (based on testCompile) and a jarTest task to create a jar from the test source set (with the classifier test added to name of the jar). We can then depend in B on the testArchives configuration of A (which will also include the transitive dependencies of A).

In A we would add the plugin to build.gradle:

apply plugin: 'com.github.hauner.jarTest'

In B we reference the testArchives configuration like this:

dependencies {
    ...
    testCompile project (path: ':ProjectA', configuration: 'testArchives') 
}
Lancey answered 30/12, 2015 at 11:58 Comment(4)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewLynching
few lines of text has been addedLancey
Anyway, information about new gradle plugin has been provided.Lancey
@Lancey Not working in Gradle 4.6, getting error Could not get unknown property 'testClasses' for project ':core' of type org.gradle.api.Project.Wollongong
C
12

I'm so late to the party (it is now Gradle v4.4) but for anyone else who finds this:

Assuming:

~/allProjects
|
|-/ProjectA/module-a/src/test/java
|
|-/ProjectB/module-b/src/test/java

Go to the build.gradle of project B (the one that needs some test classes from A) and add the following:

sourceSets {
    String sharedTestDir = "${projectDir}"+'/module-b/src/test/java'
    test {
        java.srcDir sharedTestDir
    }
}

or (assuming your project is named ProjectB)

sourceSets {
    String sharedTestDir = project(':ProjectB').file("module-b/src/test/java")
    test {
        java.srcDir sharedTestDir
    }
}

Voila!

Corella answered 22/6, 2018 at 12:11 Comment(3)
The question doesn't mention Android. Can you make your answer agnostic to whether the developer is developing for Android or not, or is it only for Android developers?Slipover
I worked on a project with cycles, so this answer is the only one that worked more or less cleanly. For reference, I put, in the project needing tests from projectExposingTests the snippet (kotlin version): sourceSets { test { java.srcDir(project(":projectExposingTests").file("src/test/java")) } }Afterbirth
Isn't this going to try to run the tests in the shared folder instead of just use them as dependencies in ProjectB's tests?Enthusiast
L
11

The Fesler's solution haven't worked for me, when i tried it to build an android project (gradle 2.2.0). So i had to reference required classes manually :

android {
    sourceSets {
        androidTest {
            java.srcDir project(':A').file("src/androidTest/java")
        }
        test {
            java.srcDir project(':A').file("src/test/java")
        }
    }
}
Lipcombe answered 5/10, 2016 at 8:13 Comment(3)
slight typo, missing the end quote after the project(':A'). This worked for me though, thanks m8Ube
For Android this idea worked beautifully for me, without the hacky feeling https://mcmap.net/q/49327/-android-multi-module-test-dependencyTehuantepec
@Tehuantepec Yes, seems as a good approach. The only limitation i see is with the @VisibleForTesting lint rules. You won't be able to call such methods from the regular module under not test folder.Lipcombe
V
7

Creating test-jar For Gradle 6.6.x

I know that there are many sources telling you, that is not OK, fe:

But this is so damn simple and I just don't like the idea of having common test classes separately in testFixtures folder.

So in module A:

task jarTests(type: Jar, dependsOn: testClasses) {
    classifier = 'tests'
    from sourceSets.test.output
}
configurations {
    tests {
        extendsFrom testRuntime
    }
}
artifacts {
    tests jarTests
}

And in module B:

testImplementation project(':moduleA')
testImplementation project(path: ':moduleA', configuration: 'tests')

And it just works!

Vella answered 9/9, 2020 at 16:27 Comment(3)
Note for IntelliJ users: when using this approach, make sure to name the configuration test. gradle command line works fine with any name, but at least IntelliJ works better with "test" (because it then understands the dependency is actually to the test sources, you dont want the IDEs to "really" depend on the output jars!)Broadloom
@Broadloom could you explain a bit more? I'm trying this approach with IntelliJ and it works. But I do not understand what you are saying. Thank you!Iconium
@Iconium Note it says configurations tests here. If I remember correctly, it should be test (singular, not plural) instead.Broadloom
F
5

If you want to use artifact dependencies to have:

  • ProjectB's source classes depend on Project A's source classes
  • ProjectB's test classes depend on Project A's test classes

then ProjectB's dependencies section in build.gradle should look something like this:

dependencies {

  compile("com.example:projecta:1.0.0")

  testCompile("com.example:projecta:1.0.0:tests")

}

For this to work ProjectA needs to build a -tests jar and include it in the artifacts it produces.

ProjectA's build.gradle should contain configuration like this:

task testsJar(type: Jar, dependsOn: testClasses) {
    classifier = 'tests'
    from sourceSets.test.output
}

configurations {
    tests
}

artifacts {
    tests testsJar
    archives testsJar
}

jar.finalizedBy(testsJar)

When ProjectA's artifacts are published to your artifactory they will include a -tests jar.

The testCompile in ProjectB's dependencies section will bring in the classes in the -tests jar.


If you want to includeFlat ProjectA's source and test classes in ProjectB for development purposes then the dependencies section in ProjectB's build.gradle would look like this:

dependencies {

  compile project(':projecta')

  testCompile project(path: ':projecta', configuration: 'tests')

}
Furnivall answered 11/2, 2019 at 23:54 Comment(3)
Unfortunately (in Gradle 6) the flat include, which was exactly what I wanted, does not work anymore because there is no configuration 'tests' anymore. Using println(configurations.joinToString("\n") { it.name + " - " + it.allDependencies.joinToString() }) (in a kotlin buildscript), I determined which configurations still exist and have dependencies, but for all of these Gradle complained: Selected configuration 'testCompileClasspath' on 'project :sdk' but it can't be used as a project dependency because it isn't intended for consumption by other components.Porkpie
This solution worked for me with Gradle 7.0Detergency
@Joman could you explain what's the difference with this other answer: https://mcmap.net/q/49004/-multi-project-test-dependencies-with-gradle? I mean, the extendFrom, the archive inside artifacts and the last part jar.finalize. Thank you (by the way I've test this and it works, but I want to understand the differences with the other answer which I've not tested yet)Iconium
C
5

If you are struggling to adapt the solution to the Gradle Kotlin DSL this is the equivalent:

configurations {
    register("testClasses") {
        extendsFrom(testImplementation.get())
    }
}
val testJar = tasks.register<Jar>("testJar") {
    archiveClassifier.set("test")
    from(sourceSets.test)
}
artifacts.add("testClasses", testJar) 
Chaker answered 9/2, 2022 at 8:57 Comment(0)
S
4

If you have mock dependencies which you need to share between tests, you can create new project projectA-mock and then add it as test dependency to ProjectA and ProjectB:

dependencies {
  testCompile project(':projectA-mock')
}

This is clear solution to share mock dependencies, but if you need to run tests from ProjectA in ProjectB use other solution.

Santalaceous answered 14/5, 2018 at 10:46 Comment(1)
Great solution for the shared mock case!Chitchat
S
4

The solution mentioned by Nikita for Android + Kotlin looks like this:

task jarTests(type: Jar, dependsOn: "assembleDebugUnitTest") {
    getArchiveClassifier().set('tests')
    from "$buildDir/tmp/kotlin-classes/debugUnitTest"
}

configurations {
    unitTestArtifact
}

artifacts {
    unitTestArtifact jarTests
}

Gradle for project that is going to use dependencies:

testImplementation project(path: ':shared', configuration: 'unitTestArtifact')
Scotney answered 7/8, 2020 at 8:45 Comment(1)
This solution works fine with Gradle, but Android Studio Electric Eel still marks the shared code as Unresolved reference.Coom
M
2

Some of the other answers caused errors one way or another - Gradle did not detect test classes from other projects or Eclipse project had invalid dependencies when imported. If anyone has the same problem, I suggest going with:

testCompile project(':core')
testCompile files(project(':core').sourceSets.test.output.classesDir)

The first line forces the Eclipse to link the other project as dependency, so all sources are included and up to date. The second allows Gradle to actually see the sources, while not causing any invalid dependency errors like testCompile project(':core').sourceSets.test.output does.

Melliemelliferous answered 24/5, 2016 at 20:14 Comment(0)
U
0

in project B:

dependencies {
  testCompile project(':projectA').sourceSets.test.output
}

Seems to work in 1.7-rc-2

Uticas answered 6/8, 2013 at 21:43 Comment(1)
It also creates unnecessary complications in the handling of the project by Eclipse. The solution suggested by @NikitaSkvortsov is preferable.Tasteful

© 2022 - 2024 — McMap. All rights reserved.