Skip to content

Latest commit

 

History

History
163 lines (121 loc) · 6.44 KB

File metadata and controls

163 lines (121 loc) · 6.44 KB

2: Your First Plugin Test

Testing Gradle plugins happens at two levels. Integration tests run a real Gradle build against a real project directory and check what came out. Unit tests exercise your logic directly, without a build at all.

Our plugin as written can only be tested at the integration level, because everything it does is tied to a Project. Tutorial 4 restructures it so that unit tests become possible. For now we will cover:

  • How to set up TestKit.
  • How to create a test Gradle project.
  • How to include your plugin in a test build.
  • How to inspect the results of that build.

Setting Up TestKit

Gradle ships a test kit built for exactly this. If you applied java-gradle-plugin in the previous tutorial, most of the wiring is already done for you: it adds the gradleTestKit() dependency to your test compile classpath, and it generates the metadata that tells GradleRunner where your plugin's classes live.

So the whole build script addition is your test framework and nothing else:

dependencies {
    testImplementation platform('org.junit:junit-bom:5.11.4')
    testImplementation 'org.junit.jupiter:junit-jupiter'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

If you are following an older tutorial, you may have seen a createClasspathManifest task that writes a plugin-classpath.txt file, plus a testRuntime files(createClasspathManifest) dependency. That was the workaround before java-gradle-plugin learned to do it itself. It is no longer needed, and testRuntime was removed from Gradle entirely. Delete both; call withPluginClasspath() with no arguments instead.

Creating a Test Project

An integration test needs a project to build. We keep two of them under testProjects/, checked into the repository so you can also run them by hand while developing.

testProjects/simpleProject/settings.gradle:

rootProject.name = 'simpleProject'

testProjects/simpleProject/build.gradle:

plugins {
    id 'io.github.intisy.myplugin'
}

Note there is no version on that plugin id, and no repository declaration. withPluginClasspath() injects the plugin directly onto the build's class path, so there is nothing to resolve.

Defining an Integration Test using GradleRunner

package io.github.intisy;

import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class TestRealBuild {
    private static final Path FIXTURES = Path.of(System.getProperty("user.dir"), "testProjects");

    @TempDir
    Path projectDir;

    private GradleRunner runnerFor(String fixture, String task) throws IOException {
        copyDirectory(FIXTURES.resolve(fixture), projectDir);
        return GradleRunner.create()
                .withProjectDir(projectDir.toFile())
                .withPluginClasspath()
                .withArguments(task, "--configuration-cache", "--stacktrace");
    }

    @Test
    void dealWithItPrintsTheGreeting() throws IOException {
        BuildResult result = runnerFor("simpleProject", "dealwithit").build();

        assertEquals(TaskOutcome.SUCCESS, result.task(":dealwithit").getOutcome());
        assertTrue(result.getOutput().contains("(•_•) ( •_•)>⌐■-■ (⌐■_■)"));
    }
}

Three things here are worth calling out.

The fixture is copied into a @TempDir rather than built in place. Running Gradle inside testProjects/simpleProject leaves a build/ directory and a .gradle/ cache behind in your source tree. Older versions of this tutorial dealt with that by deleting those directories in setUp and tearDown, which works right up until a test fails and leaves the mess anyway. Copying into a temporary directory means every test starts from a known state and cleans itself up.

withPluginClasspath() takes no arguments. It reads the metadata generated by java-gradle-plugin. There is no file for you to build or maintain.

Every run passes --configuration-cache. This is a cheap and very effective guard. Gradle's configuration cache fails the build if a task reaches back into the Project object while it is executing, which is the most common structural mistake in plugin code. Asking for it in the test suite means you find out immediately rather than from a user's bug report.

We assert SUCCESS, not UP_TO_DATE. Because the greeting lives in a doLast block, the task has work to do and reports SUCCESS. If you had written the println at configuration time as warned about in tutorial 1, the task would have no actions and report UP_TO_DATE, while the text appeared in the output of every unrelated build.

Inspecting the Results

BuildResult gives you two useful handles:

  • result.task(":taskname").getOutcome() returns the TaskOutcome: SUCCESS, UP_TO_DATE, SKIPPED, FAILED, FROM_CACHE, NO_SOURCE.
  • result.getOutput() returns the full console output as a String.

For a task that produces files, assert on the files rather than the log. Because the build ran in projectDir, the output is right where you would expect:

@Test
void myTaskWritesTheDefaultContent() throws IOException {
    BuildResult result = runnerFor("simpleProject", "mytask").build();

    assertEquals(TaskOutcome.SUCCESS, result.task(":mytask").getOutcome());
    assertEquals(\\_(ツ)_/¯",
            Files.readString(projectDir.resolve("build/myfile.txt")));
}

To assert on a build that is supposed to fail, call buildAndFail() instead of build().

Next Steps

Integration tests are slow, because each one starts a Gradle build. They are also the only way to verify that your plugin behaves correctly when a real build script applies it, so you will always want some. The trick is to keep the number small and push detail down into unit tests.

In the next step we will look at a better way to define tasks, so that a plugin can grow past a single file.