Gradle TestKit (亦称 TestKit) 是一个用于测试 Gradle 插件和通用构建逻辑的库。目前,它主要侧重于 功能 测试。也就是说,通过以编程方式执行构建来测试构建逻辑。随着时间的推移,TestKit 可能会扩展以支持其他类型的测试。

使用 TestKit

要使用 TestKit,请在您的插件构建中包含以下内容:

build.gradle.kts
dependencies {
    testImplementation(gradleTestKit())
}
build.gradle
dependencies {
    testImplementation gradleTestKit()
}

gradleTestKit() 包含了 TestKit 的类,以及 Gradle Tooling API 客户端。它不包含 JUnitTestNG 或任何其他测试执行框架的版本。此类依赖项必须显式声明。

build.gradle.kts
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.named<Test>("test") {
    useJUnitPlatform()
}
build.gradle
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

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

使用 Gradle 运行器进行功能测试

GradleRunner 促进了以编程方式执行 Gradle 构建并检查结果。

可以创建一个虚构的构建(例如,通过编程方式或从模板),用于练习“待测逻辑”。然后可以执行该构建,可能以多种方式(例如,任务和参数的不同组合)。然后可以通过断言以下内容(可能组合使用)来验证逻辑的正确性:

  • 构建的输出;

  • 构建的日志记录(即控制台输出);

  • 构建执行的任务集及其结果(例如,FAILED、UP-TO-DATE 等)。

创建并配置运行器实例后,可以根据预期结果通过 GradleRunner.build()GradleRunner.buildAndFail() 方法执行构建。

以下演示了在 Java JUnit 测试中使用 Gradle 运行器:

示例:在 Java 和 JUnit 中使用 GradleRunner

BuildLogicFunctionalTest.java
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import static org.gradle.testkit.runner.TaskOutcome.SUCCESS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class BuildLogicFunctionalTest {

    @TempDir File testProjectDir;
    private File settingsFile;
    private File buildFile;

    @BeforeEach
    public void setup() {
        settingsFile = new File(testProjectDir, "settings.gradle");
        buildFile = new File(testProjectDir, "build.gradle");
    }

    @Test
    public void testHelloWorldTask() throws IOException {
        writeFile(settingsFile, "rootProject.name = 'hello-world'");
        String buildFileContent = "task helloWorld {" +
                                  "    doLast {" +
                                  "        println 'Hello world!'" +
                                  "    }" +
                                  "}";
        writeFile(buildFile, buildFileContent);

        BuildResult result = GradleRunner.create()
            .withProjectDir(testProjectDir)
            .withArguments("helloWorld")
            .build();

        assertTrue(result.getOutput().contains("Hello world!"));
        assertEquals(SUCCESS, result.task(":helloWorld").getOutcome());
    }

    private void writeFile(File destination, String content) throws IOException {
        BufferedWriter output = null;
        try {
            output = new BufferedWriter(new FileWriter(destination));
            output.write(content);
        } finally {
            if (output != null) {
                output.close();
            }
        }
    }
}

可以使用任何测试执行框架。

由于 Gradle 构建脚本也可以用 Groovy 编程语言编写,因此通常选择用 Groovy 编写 Gradle 功能测试会更高效。此外,建议使用 (基于 Groovy 的) Spock 测试执行框架,因为它提供了许多比 JUnit 更引人注目的功能。

以下演示了在 Groovy Spock 测试中使用 Gradle 运行器:

示例:在 Groovy 和 Spock 中使用 GradleRunner

BuildLogicFunctionalTest.groovy
import org.gradle.testkit.runner.GradleRunner
import static org.gradle.testkit.runner.TaskOutcome.*
import spock.lang.TempDir
import spock.lang.Specification

class BuildLogicFunctionalTest extends Specification {
    @TempDir File testProjectDir
    File settingsFile
    File buildFile

    def setup() {
        settingsFile = new File(testProjectDir, 'settings.gradle')
        buildFile = new File(testProjectDir, 'build.gradle')
    }

    def "hello world task prints hello world"() {
        given:
        settingsFile << "rootProject.name = 'hello-world'"
        buildFile << """
            task helloWorld {
                doLast {
                    println 'Hello world!'
                }
            }
        """

        when:
        def result = GradleRunner.create()
            .withProjectDir(testProjectDir)
            .withArguments('helloWorld')
            .build()

        then:
        result.output.contains('Hello world!')
        result.task(":helloWorld").outcome == SUCCESS
    }
}

通常的做法是将任何性质更复杂的自定义构建逻辑(如插件和任务类型)作为外部类在独立项目中实现。这种方法的主要驱动力是将编译后的代码打包成 JAR 文件,发布到二进制仓库并在各种项目之间重用。

将待测插件导入测试构建中

GradleRunner 使用 Tooling API 执行构建。这意味着构建在单独的进程中执行(即与执行测试的进程不同)。因此,在调试模式下执行 测试 不允许您像预期那样调试构建逻辑。您在 IDE 中设置的任何断点都不会被测试构建执行的代码触发。

GradleRunner 支持与 Tooling API 相同的 Gradle 版本范围。支持的版本在兼容性矩阵中定义。

使用旧版本 Gradle 的构建可能仍然有效,但无法保证。

从 2.13 版本开始,Gradle 提供了一种传统机制,可将待测代码注入测试构建中。

使用 Java Gradle 插件开发插件进行自动注入

Java Gradle 插件开发插件可用于协助 Gradle 插件的开发。从 Gradle 2.13 版本开始,该插件提供了与 TestKit 的直接集成。当应用于项目时,该插件会自动将 gradleTestKit() 依赖项添加到 testApi 配置。此外,它会自动为待测代码生成类路径,并通过 GradleRunner.withPluginClasspath() 将其注入用户创建的任何 GradleRunner 实例中。需要注意的是,该机制目前在待测插件使用 plugins DSL 应用时才起作用。如果目标 Gradle 版本早于 2.8,则不会执行自动插件类路径注入。

该插件使用以下约定来应用 TestKit 依赖项和注入类路径:

  • 包含待测代码的源集:sourceSets.main

  • 用于注入插件类路径的源集:sourceSets.test

这些约定中的任何一个都可以借助 GradlePluginDevelopmentExtension 类进行重新配置。

以下基于 Groovy 的示例演示了如何通过使用 Java Gradle 插件开发插件应用的标准约定来自动注入插件类路径。

build.gradle.kts
plugins {
    groovy
    `java-gradle-plugin`
}

dependencies {
    testImplementation("org.spockframework:spock-core:2.3-groovy-4.0") {
        exclude(group = "org.codehaus.groovy")
    }
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
build.gradle
plugins {
    id 'groovy'
    id 'java-gradle-plugin'
}

dependencies {
    testImplementation('org.spockframework:spock-core:2.3-groovy-4.0') {
        exclude group: 'org.codehaus.groovy'
    }
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

示例:自动将待测代码类注入测试构建

src/test/groovy/org/gradle/sample/BuildLogicFunctionalTest.groovy
def "hello world task prints hello world"() {
    given:
    settingsFile << "rootProject.name = 'hello-world'"
    buildFile << """
        plugins {
            id 'org.gradle.sample.helloworld'
        }
    """

    when:
    def result = GradleRunner.create()
        .withProjectDir(testProjectDir)
        .withArguments('helloWorld')
        .withPluginClasspath()
        .build()

    then:
    result.output.contains('Hello world!')
    result.task(":helloWorld").outcome == SUCCESS
}

以下构建脚本演示了如何为使用自定义 Test 源集的项目重新配置 Java Gradle 插件开发插件提供的约定。

通过孵化中的 JVM Test Suite 插件,可以为建模以下 functionalTest 套件提供新的配置 DSL。
build.gradle.kts
plugins {
    groovy
    `java-gradle-plugin`
}

val functionalTest = sourceSets.create("functionalTest")
val functionalTestTask = tasks.register<Test>("functionalTest") {
    group = "verification"
    testClassesDirs = functionalTest.output.classesDirs
    classpath = functionalTest.runtimeClasspath
    useJUnitPlatform()
}

tasks.check {
    dependsOn(functionalTestTask)
}

gradlePlugin {
    testSourceSets(functionalTest)
}

dependencies {
    "functionalTestImplementation"("org.spockframework:spock-core:2.3-groovy-4.0") {
        exclude(group = "org.codehaus.groovy")
    }
    "functionalTestRuntimeOnly"("org.junit.platform:junit-platform-launcher")
}
build.gradle
plugins {
    id 'groovy'
    id 'java-gradle-plugin'
}

def functionalTest = sourceSets.create('functionalTest')
def functionalTestTask = tasks.register('functionalTest', Test) {
    group = 'verification'
    testClassesDirs = sourceSets.functionalTest.output.classesDirs
    classpath = sourceSets.functionalTest.runtimeClasspath
    useJUnitPlatform()
}

tasks.named("check") {
    dependsOn functionalTestTask
}

gradlePlugin {
    testSourceSets sourceSets.functionalTest
}

dependencies {
    functionalTestImplementation('org.spockframework:spock-core:2.3-groovy-4.0') {
        exclude group: 'org.codehaus.groovy'
    }
    functionalTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

控制构建环境

运行器通过在 JVM 临时目录(即 java.io.tmpdir 系统属性指定的路径,通常是 /tmp)内的目录中指定专用的“工作目录”,在隔离环境中执行测试构建。默认 Gradle 用户主目录(例如 ~/.gradle/gradle.properties)中的任何配置都不会用于测试执行。TestKit 不提供对环境所有方面(例如,JDK)进行细粒度控制的机制。TestKit 的未来版本将提供改进的配置选项。

TestKit 使用专用的守护进程,这些进程在测试执行后会自动关闭。

专用工作目录在构建后不会被运行器删除。TestKit 提供了两种方法来指定定期清理的位置,例如项目的构建文件夹:

设置用于测试的 Gradle 版本

Gradle 运行器需要一个 Gradle 分发版才能执行构建。TestKit 不依赖于 Gradle 的所有实现。

默认情况下,运行器将尝试根据 GradleRunner 类加载的来源查找 Gradle 分发版。也就是说,它期望该类是从 Gradle 分发版加载的,就像使用 gradleTestKit() 依赖项声明时的情况一样。

当运行器作为 由 Gradle 执行 的测试的一部分使用时(例如,执行插件项目的 test 任务),用于执行测试的相同分发版将由运行器使用。当运行器作为 由 IDE 执行 的测试的一部分使用时,导入项目时使用的 Gradle 分发版将用于测试。这意味着插件将有效地使用其构建时所用的 Gradle 版本进行测试。

或者,可以使用以下任何 GradleRunner 方法指定要使用的不同且特定的 Gradle 版本:

这可以潜在地用于跨 Gradle 版本测试构建逻辑。以下演示了一个以 Groovy Spock 测试形式编写的跨版本兼容性测试:

示例:为测试执行指定 Gradle 版本

BuildLogicFunctionalTest.groovy
import org.gradle.testkit.runner.GradleRunner
import static org.gradle.testkit.runner.TaskOutcome.*
import spock.lang.TempDir
import spock.lang.Specification

class BuildLogicFunctionalTest extends Specification {
    @TempDir File testProjectDir
    File settingsFile
    File buildFile

    def setup() {
        settingsFile = new File(testProjectDir, 'settings.gradle')
        buildFile = new File(testProjectDir, 'build.gradle')
    }

    def "can execute hello world task with Gradle version #gradleVersion"() {
        given:
        buildFile << """
            task helloWorld {
                doLast {
                    logger.quiet 'Hello world!'
                }
            }
        """
        settingsFile << ""

        when:
        def result = GradleRunner.create()
            .withGradleVersion(gradleVersion)
            .withProjectDir(testProjectDir)
            .withArguments('helloWorld')
            .build()

        then:
        result.output.contains('Hello world!')
        result.task(":helloWorld").outcome == SUCCESS

        where:
        gradleVersion << ['5.0', '6.0.1']
    }
}

使用不同 Gradle 版本进行测试时的功能支持

可以使用 GradleRunner 执行 Gradle 1.0 及更高版本的构建。但是,某些运行器功能在早期版本中不受支持。在这种情况下,当尝试使用该功能时,运行器将抛出异常。

下表列出了对所使用的 Gradle 版本敏感的功能。

表 1. Gradle 版本兼容性
功能 最低版本 描述

检查已执行的任务

2.5

检查已执行的任务,使用 BuildResult.getTasks() 和类似方法。

插件类路径注入

2.8

通过 GradleRunner.withPluginClasspath(java.lang.Iterable) 注入待测代码。

在调试模式下检查构建输出

2.9

在调试模式下运行构建时,使用 BuildResult.getOutput() 检查构建的文本输出。

自动插件类路径注入

2.13

通过应用 Java Gradle 插件开发插件,通过 GradleRunner.withPluginClasspath() 自动注入待测代码。

设置供构建使用的环境变量。

3.5

Gradle Tooling API 仅支持在更高版本中设置环境变量。

调试构建逻辑

运行器使用 Tooling API 执行构建。这意味着构建在单独的进程中执行(即与执行测试的进程不同)。因此,在调试模式下执行 测试 不允许您像预期那样调试构建逻辑。您在 IDE 中设置的任何断点都不会被测试构建执行的代码触发。

TestKit 提供了两种不同的方式来启用调试模式:

  • 将“org.gradle.testkit.debug”系统属性设置为 true,用于 使用 GradleRunner 的 JVM(即不是通过运行器执行的构建);

  • 调用 GradleRunner.withDebug(boolean) 方法。

当希望在不临时更改运行器配置的情况下启用调试支持时,可以使用系统属性方法。大多数 IDE 都提供为测试执行设置 JVM 系统属性的功能,并且可以使用此类功能来设置此系统属性。

使用构建缓存进行测试

要在测试中启用构建缓存,可以将 --build-cache 参数传递给 GradleRunner,或使用启用构建缓存中描述的其他方法。然后,当您的插件自定义任务被缓存时,您可以检查任务结果 TaskOutcome.FROM_CACHE。此结果仅对 Gradle 3.5 及更高版本有效。

示例:测试可缓存任务

BuildLogicFunctionalTest.groovy
def "cacheableTask is loaded from cache"() {
    given:
    buildFile << """
        plugins {
            id 'org.gradle.sample.helloworld'
        }
    """

    when:
    def result = runner()
        .withArguments( '--build-cache', 'cacheableTask')
        .build()

    then:
    result.task(":cacheableTask").outcome == SUCCESS

    when:
    new File(testProjectDir, 'build').deleteDir()
    result = runner()
        .withArguments( '--build-cache', 'cacheableTask')
        .build()

    then:
    result.task(":cacheableTask").outcome == FROM_CACHE
}

请注意,TestKit 在测试之间重用 Gradle 用户主目录(请参阅 GradleRunner.withTestKitDir(java.io.File)),其中包含本地构建缓存的默认位置。为了使用构建缓存进行测试,应在测试之间清理构建缓存目录。最简单的方法是将本地构建缓存配置为使用临时目录。

示例:在测试之间清理构建缓存

BuildLogicFunctionalTest.groovy
@TempDir File testProjectDir
File buildFile
File localBuildCacheDirectory

def setup() {
    localBuildCacheDirectory = new File(testProjectDir, 'local-cache')
    buildFile = new File(testProjectDir,'settings.gradle') << """
        buildCache {
            local {
                directory = '${localBuildCacheDirectory.toURI()}'
            }
        }
    """
    buildFile = new File(testProjectDir,'build.gradle')
}