Simple modern Gradle configuration to mimic default Maven Failsafe configuration

Viewed 268

What is a simple (but modern best-practices) way to configure Gradle so that it allows me to run integration tests similar to how a default Maven Failsafe configuration would work? I've read the Gradle docs for tests—in particular the Gradle docs for integration tests—but they seem pretty complicated (in comparison with Maven Failsafe), and moreover I don't believe the examples work in the same way.

What I'm looking for is pretty straightforward:

  • There would be some separate task—let's call it integrationTest for the sake of discussion.
  • The integrationTest task would not run when I invoke gradle test.
  • The integrationTest task would run (after the test task) when I invoke gradle integrationTest.
  • The integrationTest task would run (after the test task) when I invoke gradle check.
  • The integrationTest task would have identical dependencies and classpath configurations as the test task.
  • The integrationTest task would use the same source path (i.e. src/test/java) as the test task, but would only run tests ending in *IT (just to simplify the default Failsafe inclusion pattern for this discussion).
  • The test task would ignore all tests ending in *IT.

That's actually a pretty simple use case. (I just went into details so there would be no ambiguity.) I can turn that on in Failsafe using two lines to indicate goals, and three lines to indicate a dependency. Since Gradle takes the XML verbosity out of the equation, I should be able to configure that in two lines, right?

Just to be clear, I don't want to use Maven Failsafe in Gradle. I just want to configure Gradle to behave in a similar way as the default Failsafe configuration, as I detailed above.

1 Answers

in particular the Gradle docs for integration tests—but they seem pretty complicated (in comparison with Maven Failsafe)

You're comparing a plugin (Maven Failsafe) to standard/vanilla Gradle configuration. A plugin abstracts away all the underlying details so it's not fair to compare the two. If you look at the source of Maven Failsafe, you'll see it is equally 'complicated' compared to Gradle.

(...) and moreover I don't believe the examples work in the same way.

The docs for configuring integration test work for typical applications. However since you have specific needs, so you'll need to adapt them.

Since Gradle takes the XML verbosity out of the equation, I should be able to configure that in two lines, right?

No because again, you're comparing a plugin to standard/vanilla Gradle configuration. XML vs Groovy/Kotlin DSL is up for debate which is better or worse and is a matter of opinion.

For your requirements, I believe I've captured them below (untested):

sourceSets {
    create("integrationTest") {
        compileClasspath += sourceSets.test.get().output
        runtimeClasspath += sourceSets.test.get().output
    }
}

val integrationTestImplementation by configurations.getting {
    extendsFrom(configurations.testImplementation.get())
}

configurations["integrationTestRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get())

val integrationTest by tasks.registering(Test::class) {
    description = "Runs integration tests."
    group = "verification"
    testClassesDirs = sourceSets["integrationTest"].output.classesDirs
    classpath = sourceSets["integrationTest"].runtimeClasspath
    filter {
        includeTestsMatching("IT*")
        includeTestsMatching("*IT")
        includeTestsMatching("*ITCase")
    }
    shouldRunAfter("test")
}

tasks {
    test {
        filter {
            excludeTestsMatching("IT*")
            excludeTestsMatching("*IT")
            excludeTestsMatching("*ITCase")
        }
    }
    register("integrationTest", Test::class) {
        description = "Runs integration tests."
        group = "verification"
        testClassesDirs = sourceSets["integrationTest"].output.classesDirs
        classpath = sourceSets["integrationTest"].runtimeClasspath
        filter {
            includeTestsMatching("IT*")
            includeTestsMatching("*IT")
            includeTestsMatching("*ITCase")
        }
        shouldRunAfter(test)
    }
}

Although this is a bit verbose/awkward since you want integration tests to be within the same source set as test. So the above can be simplified to just:

tasks {
    test {
        filter {
            excludeTestsMatching("IT*")
            excludeTestsMatching("*IT")
            excludeTestsMatching("*ITCase")
        }
    }
    register("integrationTest", Test::class) {
        description = "Runs integration tests."
        group = "verification"
        testClassesDirs = sourceSets.test.get().output.classesDirs
        classpath = sourceSets.test.get().runtimeClasspath
        filter {
            includeTestsMatching("IT*")
            includeTestsMatching("*IT")
            includeTestsMatching("*ITCase")
        }
        shouldRunAfter(test)
    }
}
Related