EDIT: Clean approach with convention plugin, see below for old (discouraged) approach.
Main idea: https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:convention_plugins.
In buildSrc/build.gradle:
plugins {
id 'groovy-gradle-plugin'
}
Create a plugin which defines the dependencies we need for any test.
In buildSrc/src/main/groovy/playground.tests.gradle:
plugins {
id 'java'
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:latest.release'
testImplementation 'org.assertj:assertj-core:latest.release'
testImplementation 'nl.jqno.equalsverifier:equalsverifier:latest.release'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:latest.release'
}
tasks.withType(Test) {
useJUnitPlatform()
}
Make use of this plugin, and expose the test classes added to :architecture-tests.
In architecture-tests/build.gradle:
plugins {
id 'playground.tests' // so that we have access to JUnit, AssertJ, ...
}
dependencies {
// add whatever you need to write your architecture tests
}
// provide configurations that can be consumed by the other subprojects
configurations {
exposedTestClasses {
canBeConsumed = true
canBeResolved = false
}
exposedTestRuntimeClasspath {
canBeConsumed = true
canBeResolved = false
}
}
// wire the required data to the exposed configurations
artifacts {
exposedTestClasses(sourceSets.test.output.classesDirs.files) {
// make sure the compilation task is run first
builtBy(compileTestJava)
}
exposedTestRuntimeClasspath sourceSets.test.runtimeClasspath.files
}
Provide the test classes in architecture-tests/src/test/java/.
Now define a plugin that can be used to import the architecture tests.
In buildSrc/src/main/groovy/playground.architecture-tests.gradle:
plugins {
id 'playground.tests' // inherit the basic test dependencies
}
// define the configurations that connect the task (defined below) to the exposed configurations of `:architecture-tests`
configurations {
sharedTestClasses {
canBeConsumed = false
canBeResolved = true
}
sharedTestRuntimeClasspath {
canBeConsumed = false
canBeResolved = true
extendsFrom testRuntimeClasspath
}
}
dependencies {
sharedTestClasses project(path: ':architecture-tests', configuration: 'exposedTestClasses')
sharedTestRuntimeClasspath project(path: ':architecture-tests', configuration: 'exposedTestRuntimeClasspath')
}
task sharedTest(type: Test) {
description = 'Runs the tests shared by all subprojects.'
group = 'verification'
// use the configurations defined above, which depends on the configurations in `:architecture-tests`
testClassesDirs = configurations.sharedTestClasses
classpath = configurations.sharedTestRuntimeClasspath
// optional - extend the classpath with the subproject's test and integration test classpath, so that I'm able to access these tests from my shared test
classpath += sourceSets.test.runtimeClasspath
classpath += sourceSets.integrationTest.runtimeClasspath
}
check.dependsOn sharedTest
To run the architecture tests as part of a subproject,
in subproject/build.gradle add:
plugins {
id 'playground.architecture-tests'
}
EDIT: Older, discouraged, solution:
I think I figured it out. I'd be very happy to receive comments and ideas for improvements!
I have a subproject :architecture-tests that contains the tests I'd like to include in every other subproject. I need to have a dedicated subproject for this, as I'm changing the configuration of the regular subprojects both in their subproject-xxx/build.gradle files (subproject configuration time?) and inside a subprojects {} block in the root build.gradle file (project configuration time?). This hasn't been an issue so far, but I'm unable to reconfigure a subproject after any of the exposed/consumable configurations is resolved by another subproject. As such, I need to exclude the :architecture-tests subproject from this subprojects block and configuration, and copy the necessary bits of configuration into architecture-tests/build.gradle. If I don't do this, I get:
"Cannot change role of dependency configuration ':architecture-tests:testSharedRuntimeClasspath' after it has been resolved."
Here's my solution:
Place the test(s) in architecture-tests/src/testShared/java/.
architecture-tests/build.gradle:
plugins {
id 'java-library'
}
// I don't think it is necessary to have a special source set, though
sourceSets {
testShared {
}
}
// provide configurations that can be consumed by the other subprojects
configurations {
exposedTestSharedClasses {
canBeConsumed = true
canBeResolved = false
}
exposedTestSharedRuntimeClasspath {
canBeConsumed = true
canBeResolved = false
}
}
// just add some regular dependences for my shared tests
dependencies {
// I use a platform which defines the versions/constraints. You can just add the versions below without using a platform.
testSharedImplementation platform(project(':some-platform'))
testSharedImplementation 'org.testng:testng'
testSharedImplementation 'org.assertj:assertj-core'
testSharedImplementation 'io.github.classgraph:classgraph'
}
// wire the required data to the exposed configurations
artifacts {
// give access to the compiled (test) classes
exposedTestSharedClasses(sourceSets.testShared.output.classesDirs.files) {
// make sure the compilation task is run first
builtBy(compileTestSharedJava)
}
// give access to the runtime classpath, containing the libraries defined above
exposedTestSharedRuntimeClasspath sourceSets.testShared.runtimeClasspath.files
}
For each consuming (other) subproject (which I do in the root build.gradle in a subprojects{} block:
// define the configurations that connect the task (defined below) to the exposed configurations of `:architecture-tests`
configurations {
testSharedClasses {
canBeConsumed = false
canBeResolved = true
}
testSharedRuntimeClasspath {
canBeConsumed = false
canBeResolved = true
extendsFrom testRuntimeClasspath
}
}
dependencies {
testSharedClasses project(path: ':architecture-tests', configuration: 'exposedTestSharedClasses')
testSharedRuntimeClasspath project(path: ':architecture-tests', configuration: 'exposedTestSharedRuntimeClasspath')
}
task testShared(type: Test) {
useTestNG()
description = 'Runs the tests shared by all subprojects.'
group = 'verification'
// use the configurations defined above, which depends on the configurations in `:architecture-tests`
testClassesDirs = configurations.testSharedClasses
classpath = configurations.testSharedRuntimeClasspath
// optional - extend the classpath with the subproject's test and integration test classpath, so that I'm able to access these tests from my shared test
classpath += sourceSets.test.runtimeClasspath
classpath += sourceSets.integrationTest.runtimeClasspath
}
// run the shared tests as part of the regular `build` (which depends on `check`)
check.dependsOn testShared