Part 6: Writing a Functional Test
Learn how to write a functional test to verify your plugin’s behavior in a real-world scenario.
Step 1: About Functional Tests
While a unit test checks a single class in isolation, a functional test verifies that your plugin works correctly in a real Gradle build.
Gradle provides a powerful testing framework called TestKit. It allows you to programmatically:
-
Spin up a real Gradle process.
-
Create a temporary, isolated project directory.
-
Run a build in that directory with a specific set of tasks and arguments.
-
Verify the build’s output, status, and side effects (like files being created).
The gradle init
command created a sample functional test for you:
It's available at `src/functionalTest/kotlin/org/example/PluginTutorialPluginFunctionalTest.kt`.
It's available at `src/functionalTest/groovy/org/example/PluginTutorialPluginFunctionalTest.groovy`.
It uses the GradleRunner
class to execute a build and then asserts that the build’s output contains the expected "Hello World" message.
We’re going to re-use this same structure for our test.
Step 2: Update the Test
First, let’s update the name of the test file to match our plugin.
Rename the file `PluginTutorialPluginFunctionalTest.kt` to `SlackPluginFunctionalTest.kt`.
Rename the file `PluginTutorialPluginFunctionalTest.groovy` to `SlackPluginFunctionalTest.groovy`.
Now, update the test file to look like this:
package org.example
import java.io.File
import kotlin.test.assertTrue
import kotlin.test.Test
import org.gradle.testkit.runner.GradleRunner
import org.junit.jupiter.api.io.TempDir
class SlackPluginFunctionalTest {
@field:TempDir
lateinit var projectDir: File
private val buildFile by lazy { projectDir.resolve("build.gradle") }
private val settingsFile by lazy { projectDir.resolve("settings.gradle") }
@Test fun `can run task`() {
// Set up the test build
settingsFile.writeText("")
buildFile.writeText("""
plugins {
id('org.example.slack')
}
slack {
token.set(System.getenv("SLACK_TOKEN"))
channel.set("#social")
message.set("Hello from Gradle!")
}
""".trimIndent()
)
// Run the build
val runner = GradleRunner.create()
runner.forwardOutput()
runner.withPluginClasspath()
runner.withProjectDir(projectDir)
val result = runner.build()
// Verify the result
assertTrue(result.output.contains("Slack message sent successfully"))
}
}
package org.example
import spock.lang.Specification
import spock.lang.TempDir
import org.gradle.testkit.runner.GradleRunner
class SlackPluginFunctionalTest extends Specification {
@TempDir
private File projectDir
private getBuildFile() {
new File(projectDir, "build.gradle")
}
private getSettingsFile() {
new File(projectDir, "settings.gradle")
}
def "can run task"() {
given:
settingsFile << ""
buildFile << """
plugins {
id('org.example.slack')
}
slack {
token.set(System.getenv("SLACK_TOKEN"))
channel.set('#social')
message.set('Hello from Gradle!')
}
"""
when:
def runner = GradleRunner.create()
runner.forwardOutput()
runner.withPluginClasspath()
runner.withProjectDir(projectDir)
def result = runner.build()
then:
result.output.contains("Slack message sent successfully")
}
}
This functional test does the following:
-
Creates a temporary test project (
@TempDir
), complete withsettings.gradle
and abuild.gradle
file. -
Applies the Slack plugin and configures it with a token and channel. The token is retrieved from an environment variable, just like a real-world setup.
-
Runs the build using
GradleRunner
, with our custom tasksendTestSlackMessage
as the argument. -
Verifies the result by asserting that the build output contains
"Slack message sent successfully"
.
Step 3: Check out the Functional Test Build Logic
Before you run the test, it’s good to understand how gradle init
configured our project to support functional tests.
Look at the build.gradle(.kts)
file and notice the section related to functional tests:
// Add a source set for the functional test suite
val functionalTestSourceSet = sourceSets.create("functionalTest") {
}
configurations["functionalTestImplementation"].extendsFrom(configurations["testImplementation"])
configurations["functionalTestRuntimeOnly"].extendsFrom(configurations["testRuntimeOnly"])
// Add a task to run the functional tests
val functionalTest by tasks.registering(Test::class) {
testClassesDirs = functionalTestSourceSet.output.classesDirs
classpath = functionalTestSourceSet.runtimeClasspath
useJUnitPlatform()
}
gradlePlugin.testSourceSets.add(functionalTestSourceSet)
tasks.named<Task>("check") {
// Run the functional tests as part of `check`
dependsOn(functionalTest)
}
tasks.named<Test>("test") {
// Use JUnit Jupiter for unit tests.
useJUnitPlatform()
}
// Add a source set for the functional test suite
sourceSets {
functionalTest {
}
}
configurations.functionalTestImplementation.extendsFrom(configurations.testImplementation)
configurations.functionalTestRuntimeOnly.extendsFrom(configurations.testRuntimeOnly)
// Add a task to run the functional tests
tasks.register('functionalTest', Test) {
testClassesDirs = sourceSets.functionalTest.output.classesDirs
classpath = sourceSets.functionalTest.runtimeClasspath
useJUnitPlatform()
}
gradlePlugin.testSourceSets.add(sourceSets.functionalTest)
tasks.named('check') {
// Run the functional tests as part of `check`
dependsOn(tasks.functionalTest)
}
tasks.named('test') {
// Use JUnit Jupiter for unit tests.
useJUnitPlatform()
}
This configuration sets up a dedicated functionalTest
source set and task, which keeps our functional tests separate from our unit tests.
This makes our test suites more organized and reliable.
Step 4: Run the Functional Test
The functional test uses an environment variable named SLACK_TOKEN
.
Before you run the test, you must set this variable in your terminal.
$ export SLACK_TOKEN="xoxb-..."
Now you can run the functionalTest
task to execute your test.
$ ./gradlew :functionalTest
> Task :plugin:functionalTest
BUILD SUCCESSFUL in 4s
6 actionable tasks: 1 executed, 5 up-to-date
If everything is configured correctly, you should see a message confirming a successful build, and a test message should have been sent to your Slack workspace!
You can also run the check
task, which executes both unit and functional tests.
Next Step: Use a Consumer Project >>