Gradle: How can I run a specific (architecture) test for each subproject?

Viewed 371

I have a Gradle monolith project with around 50 subprojects. I'd like to ensure that the code in each subproject conforms to a specific rule, for example that each class named *Foo is annoted with Bar.

I already wrote a test that successfully scans the classpath and asserts the property I'm interested in.

Where do I put this code so that it is executed for the whole project, either individually for each subproject, or once for the whole codebase?

I know about test fixtures, so I could easily provide the logic in a class that is accessible by all subprojects. However, I'd still need to write a test class that runs the shared code, which I'd have to do for each of my subprojects.

How can I avoid this duplication? I'm looking for something like this:

subprojects {
    additionalTestsFrom project(':architecture-tests')
}
1 Answers

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
Related