2

My test source structure is as follows:

   test
   testFunctional
   testIntegration
   testSupport

The testSupport folder contains common code to be shared among all test folders.

The definition of the source sets in gradle looks like below:


    test {
        java {
            srcDirs = ['src/test/java', 'src/testSupport/java']
        }
        resources {
            srcDirs = ['src/test/resources']
        }
    }

    functionalTest {
        java {
            srcDirs = ['src/testFunctional/java', 'src/testSupport/java']
        }
        resources {
            srcDirs = ['src/testFunctional/resources']
        }
    }

    integrationTest {
        java {
            srcDirs = ['src/testIntegration/java', 'src/testSupport/java']
        }
        resources {
            srcDirs = ['src/testIntegration/resources']
        }
    }
}

IntelliJ currently complains that "Duplicate content roots detected" referring to the fact that the same 'src/testSupport/java' folder is shared among multiple source sets.

Do you have a more elegant solution to achieve sharing code among test folder in gradle without going to a multi-module approach?

Dio
  • 684
  • 2
  • 9
  • 18
  • Intellij at the moment does not allow a file to belong to multiple modules. Please see [this comment](https://youtrack.jetbrains.com/issue/IDEABKL-6745#focus=streamItem-27-2672229.0-0) for discussion. – Andrey Jan 21 '20 at 12:41

1 Answers1

1

I would like to start by saying you should use Gradle Test Fixtures, at the time of your question this wasn't possible, but nowadays it works pretty well.


That said, this problem will still come up, for example if you want to share code between suites of JVM Test Suite plugin. The plugin solves a lot of problems out of the box; sadly not the duplicate sources one.

Below are three alternative solutions for the problem:

  1. Share testFixtures in test suites
  2. Copy the source files and compile them multiple times.
  3. Share the tests via testFixtures.

Share testFixtures in test suites

The best solution, combining the two built-in Gradle plugins.

testing.suites.named<JvmTestSuite>("...").configure { // this: JvmTestSuite
    dependencies {
        implementation(project())
        implementation(testFixtures(project()))
    }
}

Copy the source files and compile them multiple times.

I had a similar problem here and solved it with a copy. Adopting it to your example above it would be something like:

val copyFunctionalTestSupportClasses by tasks.registering<Copy>() {
    from("src/testSupport/java")
    into("src/testFunctional-testSupport/java")
}
java.sourceSets.named("functionalTest").configure {
    java.srcDir(copyFunctionalTestSupportClasses)
}
val copyIntegrationTestSupportClasses by tasks.registering<Copy>() {
    from("src/testSupport/java")
    into("src/testIntegration-testSupport/java")
}
java.sourceSets.named("integrationTest").configure {
    java.srcDir(copyIntegrationTestSupportClasses)
}
java.sourceSets.named("test").configure {
    // Leave it alone, use the original;
    java.srcDir("src/testSupport/java")
    // or do the same as the others to improve consistency between all 3 test types.
}

... yes, you're seeing it right, the srcDir gets a TaskProvider instance; it'll execute the task when necessary.


Share the tests via testFixtures.

This is more useful if you actually want to run shared tests in multiple tasks, which looking at your setup, you probably don't want. I'll put it here anyway in case someone is looking for it (me in the future ).

  1. Move the common tests to testFixtures
  2. Configure the Test task with
....configure { // this: Test
    (testClassesDirs as ConfigurableFileCollection)
        .from(sourceSets["testFixtures"].output.classesDirs)
}

This will make the Gradle test runner scan the testFixtures classes for @Tests. This is weird though, because the fixtures shouldn't contain tests.

The proper solution is this:

java {
    registerFeature("sharedTests") {
        usingSourceSet(sourceSets.create("sharedTest"))
        disablePublication()
    }
}
dependencies {
    "sharedTestImplementation"(project)
    "sharedTestImplementation"(testFixtures(project))
    "sharedTestImplementation"(...) // any normal dependencies
}
....configure { // this: Test
    // This adds the @Test classes from sharedTest to each configured Test class.
    testClassesDirs = files(
        testClassesDirs, // Keep original.
        sourceSets["sharedTest"].output.classesDirs,
    )
}
dependencies {
    // Add a dependency on the new "feature" to the classpath of the Test task.
    implementation(project) { // or project() for TestSuites
        capabilities {
            // Sadly an internal API need to be used,
            requireCapability(org.gradle.internal.component.external.model.ProjectDerivedCapability.ProjectDerivedCapability(project, "sharedTests"))
            // or, if you prefer, hard-code it:
            requireCapability("${project.group}:${project.name}-shared-tests")
        }
    }
}
TWiStErRob
  • 44,762
  • 26
  • 170
  • 254