I can't seem to share test code between projects in a multi-project build

I have a (poorly organized) project that I’m converting from Maven. In it there is a module that has tests that depend on test classes from a sibling project. I’ve tried to set up the dependencies so this works, but I’m still getting errors when one project’s test code attempts to import classes from another project’s test code.

E.g. assume two projects: Server and ServerWeb

In build.gradle for ServerWeb there are dependencies on Server

apply plugin: 'war’
apply plugin: ‘gwt’

dependencies {
providedCompile project(’:Server’)
testCompile project(path: ‘:Server’, configuration: ‘testCompile’)
}

When it compiles the test classes for ServerWeb, it fails to import test classes from Server. I can see that the classes are there in the Server project when I look in Server/build/classes/test/

I also tried depending on ‘testRuntime’ instead of ‘testCompile’, but it made no difference.
I’m running Gradle 2.11.

Is there a trick to getting this to work? I obviously expected what I had to do the trick, so maybe there is a bug somewhere… either in my script or in how Gradle handles the class path for tests.

you need the compiled class files in your test class path:

dependencies {
  ...
  testCompile project(':Server').sourceSets.test.output
}
1 Like

In your Server project:

configurations {
    testArtifacts.extendsFrom testCompile
}
task testJar(type: Jar) {
    classifier "test"
    from sourceSets.test.output
}
artifacts {
    testArtifacts testJar
}

In your ServerWeb project:

testCompile project(path: ":Server", configuration: 'testArtifacts')
1 Like

Thanks to both of you. I went with Nikolay’s solution as I would like to avoid test artifact. I would like to refactor things so this dependency isn’t required at some point anyway.

Getting error on sourceSets.test.output for both the solutions,

Could not get unknown property 'test' for SourceSet container of type org.gradle.api.internal.tasks.DefaultSourceSetContainer.

can anyone kindly help me
I am using Gradle 4.6 in android studio 3.2.1

2 Likes

Any luck guys? I am having the same issue.

1 Like

Also having this issue in Gradle 5. Would like to know the latest best practice with this common use case.

I’ve found that referencing test classes from another project is an anti-pattern that should be avoided. I consider anything under src/test/** is private to that project. When you think about it, you actually want to share the test utilities from another project, you don’t want the actual tests on the other project’s classpath

Instead, it’s best to put common test utilities (abstract base classes, common assertions, mocks, common resources etc) in their own project (under src/main/**) which can be referenced as a normal library in other projects (as a test dependency)

This follows the same pattern as other test libraries (eg mockito, junit, assertj etc)

1 Like

@Lance, that would be wrong, because that way gw assemble also compiles test code, which shouldn’t be the case. I still think the test jar approach is the cleanest.

1 Like

It is an anti-pattern because the tools do a poor job supporting it. Your " I consider anything under src/test/** is private to that project" is an assertion, not an argument. A counter-argument is a project that defines an “abstraction” along with tests for the abstraction, then another project that extends that abstraction, and needs to add additional tests, as well as prove the original test still work with the extension. Any use of inheritance between the projects could result in a corresponding inheritance in the test classes. To make a 3rd project just to handle shared test code is also a code-smell.

Not trying to start a big academic argument as we’re all in this “battle against code smell” together. I found this thread because I am looking for discussion of “.sourceSets.test.output”. The great problem with this, at least in my eclipse ide, is that it adds a dependency in the .classpath file pointing to the dependent project’s build/classes/java/test folder. And that means code changes in the dependee project require the gradle assemble task to propagate. That defeats the purpose of the IDE.

1 Like

It’s an anti-pattern because that folder is for unit tests of the project that it is a part of. No other project should be concerned with that.

If you want integration tests with other modules, it is perfectly reasonable to create a separate test project to validate that the other modules work together.
If you want a helper library for tests, that is just it’s own project with public API that isn’t part of the “test” source.

I tried @swpalmer’s approach of moving common test helpers into a separate project, as regular sources, and depending on it from the test projects. Unfortunately it broke down because I’m using the Java 9 module system (which I have regretted doing multiple times) and am getting circular dependency errors.

The approach of adding the test sourceSet output as a testCompile dependency didn’t work for me (Gradle 6 here) either. The separate test jar artifact didn’t do the trick either, as it will also conflict with module semantics. Testing really seems like an afterthought in the whole Java 9 modules business.

Sounds like you might need to use --patch-module.
https://openjdk.java.net/projects/jigsaw/quick-start#xoverride

The module system can be a pain in the butt… but I think ultimately it leads you to better code organization and designs.

Thanks, will give it a try. I’m aware of the option but I’m not sure if it will help. And yes, the module system is a PITA… but it’s really useful when you intend to use jlink in a later stage and ship a self-contained application. The reduction in size is quite significant in our case.

Actually if you know which modules you need, you can use jlink and give it an explicit list of modules to include, that way you can build a custom distribution even without using JPMS everwhere.

But besides that, for common test helpers or common fixtures and similar, there is now the java-text-fixtures plugin where you can put test code that should be shared with other projects.

And regarding testing with JPMS, you can do white-box tests by running your tests non-modular (with Gradle just not having a module-info.java in the source set should be sufficient for Gradle to run the tests on the class path, not the module path, so you are not bound to module boundaries. And if you want to do black-box tests with the JPMS restrictions, you have a module-info.java in the test source set and so it is a module of its own and can interact with the SUT acting like a module. If you want both, you just can have two source sets and two test tasks for these.