How to share tests between multiple projects?

Greetings,

We’re struggling to setup a project where two subprojects must share a set of classes, and related test classes.

Detailed problem:

The root project R contains 3 java subprojects A, B, C. Both projects A and B are applications which depend on C. And C contains common code (but there is no dependency between A and B).
A lot of test classes target code defined in C, but they can yield different results wether they are run from A or from B. We don’t want to copy theses test classes into A or B, since they are common (and target classes in C). We want a single test codebase for them.
And we want to be able to launch a single test class (or test package) from within Eclipse, either from project A or project B.

Our imperfect solution:

The setup we use at the moment can be changed, it works with gradle eclipse plugin, but not with buildship (see below). Also note that it has been built before Eclipse allowed test code separation, so we could do some improvements on this topic.
Here is what we do at the moment:

We created a 4th subproject T, which contains only these common test classes. and we put the test classes in “src/main/java”.
In projects A and B, we define a test dependency to this project T, and we add the output folder to the tests. Example for project A:
project(":A").test.testClassesDirs += project(":T").sourceSets.main.output
This work perfectly fine with pure gradle builds:
gradlew :A:build

But then, how do we launch a single test class from Eclipse using either gradle or JUnit ? We cannot right-click a test class in project T, and “run as -> gradle test” or “run as -> junit test”. Even if these classes were in the test classes of project C (in “src/test/java”), Eclipse cannot tell if we want to run them from A or from B (running them from C itself won’t work because we need to load some implementation classes from A or from B to make things work).
The trick we use is to add some Eclipse specifics. Example for project A:

def commonTestsDir = /* path to src/main/java of project T */
eclipse {
    project {
        linkedResource name: 'common-tests', type: '2', location: commonTestsDir
    }
    classpath.file.withXml {
        def node = it.asNode()
        node.appendNode('classpathentry', [kind: 'src', path: 'common-tests', exported: true])
    }
}

Doing this on both A and B, then running “gradle eclipse” will happily show a folder link “common-tests” under each projects (A and B) where we can browse for test classes, and run them from right-clicks, while allowing Eclipse to know if we’re in A or B.

Note that we simultaneously have a project dependency to T, explicitely added so that “gradlew test” works fine AND a classpath dependency added to Eclipse buildpath with our customization added to gradle eclipse plugin. This works like a charm up to this stage.

Our issue with buildship:

But this doesn’t work with buildship. If we “Refresh gradle project”, we still have the project dependency, but the Eclipse classpath dependency, but the Eclipse classpath dependency gets lost.
We also tried to use “minusConfiguration” to remove the project dependency on the Eclipse classpath (so that the classpath dependency could be taken into account again), but this had no effect.
As a result, we can list our “common-tests” files from A or B, but since they’re not in the Eclipse classpath, we cannot right-click them to launch tests.

We did setup this originally with gradle 4.1 and buildship 2.1.2, and we now moved to gradle 6.3 and we tried with buildship 2.1.2 and buildship 3.1.4.

Everything can probably be done without project T, by moving the test classes to project C in “src/test/java”. But if I’m not mistaken, I don’t think that will solve any issue.

Any hints would be really appreciated. Thanks in advance.

Ideally you’d only have utility classes and abstract base test classes in project T. Then you’d have the actual concrete test cases in both A and B.

Can you tell us a bit more about how the same test tests acts differently in A and B? Perhaps you could remove this “magic” switch and instead extend an abstract base test in both A and B.

The tests in A and B could be simple 3 liner classes which extend a base and set a property or provide an extra annotation etc

Another option is to copy the tests from project T into projects A & B under a “$buildDir/generated” folder prior to test compile. You could then run the tests in your IDE via the “generated” versions of each test case

Eg:

[':A', ':B'].each {
   project(it) {
      apply plugin: 'java'
      task generateTestSources(type: Copy) {
         ext.outputDir = "$buildDir/generated/test/java"
         from project(':T').file('src/main/java')
         into outputDir
      }
      compileTestJava.dependsOn generateTestSources
      sourceSets.test.java.srcDir generateTestSources.outputDir
   }
}

Bonus points for

  • Repackaging the tests into A and B packages as part of the copy (this will help junit reporting)
  • Setting the Eclipse’s “derived” flag on the $buildDir/generated/test/java" folders
  • Calling File.setReadOnly() on the generated files so you don’t accidentally attempt to edit them (and subsequently have your changes overridden next copy)

Thank you very much Lance, I like the copy at build time. I’ll give it a try.

I didn’t give a concrete example because it was already a long OP, but of course, I can elaborate a bit more.

In project C, I have interfaces such as:

public interface MyService {
    int add(int foo, int bar);
}

In project T, I can test an implementation:

public class MyServiceTest {
    @Test
    public void testAdd() {
        MyService service = load(MyService.class);
        int result = service.add(1, 2);
        Assert.assertEquals(3, result);
    }    
}

There is some “magic” here for the “load” method. There are many ways to make this magic work, and I won’t extend on this, but java ServiceLoader (SPI) or any injection framework (spring, CDI…) would do it.
And as you will guess, both project A and B have different implementations of “MyService”, so running the test from A or from B will load a different implementation and may lead to different test results.

I think I have a similar issue, possibly with some insights that might help in your case. I don’t need to be able to see those tests in my IDE, though. Instead it suffices if they are executed as part of ./gradlew build or even ./gradlew subproject:check.