Gradle TestKit(又名 TestKit)是一个帮助测试 Gradle 插件和构建逻辑的库。此时,重点是功能测试。也就是说,通过将构建逻辑作为以编程方式执行的构建的一部分来进行测试。随着时间的推移,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 构建并检查结果。

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

  • 构建的输出;

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

  • 构建执行的任务集及其结果(例如失败、最新等)。

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

下面演示了 Gradle runner 在 Java JUnit 测试中的用法:

示例:将 GradleRunner 与 Java 和 JUnit 结合使用

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 相比,它提供了许多引人注目的功能。

下面演示了Gradle runner在Groovy Spock测试中的用法:

示例:将 GradleRunner 与 Groovy 和 Spock 结合使用

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 使用工具 API来执行构建。这意味着构建是在单独的进程中执行的(即与执行测试的进程不同)。因此,测试构建不会与测试进程共享相同的类路径或类加载器,并且被测代码不会隐式地可供测试构建使用。

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

使用较旧的 Gradle 版本进行构建可能仍然有效,但不能保证。

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

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

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

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

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

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

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

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

dependencies {
    testImplementation("org.spockframework:spock-core:2.2-groovy-3.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.2-groovy-3.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
}

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

通过孵化JVM 测试套件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.2-groovy-3.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.2-groovy-3.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 提供了两种方法来指定定期清理的位置,例如项目的构建文件夹:

  • 系统org.gradle.testkit.dir属性;

  • GradleRunner.withTestKitDir (file testKitDir)方法。

用于测试的Gradle版本

Gradle 运行程序需要 Gradle 发行版才能执行构建。 TestKit 并不依赖于 Gradle 的所有实现。

默认情况下,运行程序将尝试根据类GradleRunner的加载位置查找 Gradle 发行版。也就是说,预计该类是从 Gradle 发行版加载的,就像使用gradleTestKit()依赖项声明时的情况一样。

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

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

这有可能用于测试跨 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 Plugin Development 插件,通过GradleRunner.withPluginClasspath()自动注入被测代码。

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

3.5

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

调试构建逻辑

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

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

  • 使用JVM设置“ org.gradle.testkit.debug”系统属性(即不是使用运行器执行构建);trueGradleRunner

  • 调用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 User Home(请参阅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')
}