Learn how to write a functional test to verify your plugin’s behavior in a real-world scenario.

In this section, you’ll:

  • Understand the difference between unit and functional tests.

  • Learn how to use Gradle’s TestKit for end-to-end testing.

  • Update the default functional test to verify your plugin.

  • Run the functional test and confirm your plugin works as expected.

Step 0: Before you Begin

  1. You initialized your plugin in part 1.

  2. You added an extension to your plugin in part 2.

  3. You created a custom task in part3.

  4. You wrote a unit test in part 4.

  5. You added a dataflow action to your plugin in part 2.

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:

plugin/src/functionalTest/kotlin/org/example/SlackPluginFunctionalTest.kt
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"))
    }
}
plugin/src/functionalTest/groovy/org/example/SlackPluginFunctionalTest.groovy
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:

  1. Creates a temporary test project (@TempDir), complete with settings.gradle and a build.gradle file.

  2. 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.

  3. Runs the build using GradleRunner, with our custom task sendTestSlackMessage as the argument.

  4. 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:

plugin/build.gradle.kts
// 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()
}
plugin/build.gradle
// 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 >>