Gradle TestKit(又名TestKit)是一个有助于测试Gradle插件和一般构建逻辑的库. 目前,它专注于功能测试. 也就是说,通过将其作为程序执行的构建的一部分进行测试来测试构建逻辑. 随着时间的流逝,TestKit可能会扩展以方便其他类型的测试.

Usage

要使用TestKit,请在插件的版本中包含以下内容:

示例1.声明TestKit依赖项
build.gradle
dependencies {
    testImplementation gradleTestKit()
}
build.gradle.kts
dependencies {
    testImplementation(gradleTestKit())
}

gradleTestKit()包含gradleTestKit()的类以及Gradle Tooling API客户端 . 它不包括JUnitTestNG或任何其他测试执行框架的版本. 必须明确声明这种依赖性.

示例2.声明JUnit依赖项
build.gradle
dependencies {
    testImplementation 'junit:junit:4.12'
}
build.gradle.kts
dependencies {
    testImplementation("junit:junit:4.12")
}

Functional testing with the Gradle runner

GradleRunner有助于以编程方式执行Gradle构建并检查结果.

可以(例如以编程方式或从模板中)创建人为构建的练习"被测逻辑". 然后可以潜在地以各种方式(例如,任务和参数的不同组合)执行构建. 然后可以通过断言以下内容(可能组合使用)来验证逻辑的正确性:

  • 构建的输出;

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

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

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

以下内容演示了Java JUnit测试中GradleRunner的用法:

Example: Using GradleRunner with JUnit

BuildLogicFunctionalTest.java
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import static org.gradle.testkit.runner.TaskOutcome.*;

public class BuildLogicFunctionalTest {
    @Rule public final TemporaryFolder testProjectDir = new TemporaryFolder();
    private File settingsFile;
    private File buildFile;

    @Before
    public void setup() throws IOException {
        settingsFile = testProjectDir.newFile("settings.gradle");
        buildFile = testProjectDir.newFile("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.getRoot())
            .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实现的,所以用Groovy编写Gradle功能测试通常是一种有效的选择. 此外,建议使用(基于Groovy的) Spock测试执行框架,因为它比JUnit的使用具有许多引人注目的功能.

以下内容演示了Groovy Spock测试中Gradle Runner的用法:

Example: Using GradleRunner with Spock

BuildLogicFunctionalTest.groovy
import org.gradle.testkit.runner.GradleRunner
import static org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification

class BuildLogicFunctionalTest extends Specification {
    @Rule TemporaryFolder testProjectDir = new TemporaryFolder()
    File settingsFile
    File buildFile

    def setup() {
        settingsFile = testProjectDir.newFile('settings.gradle')
        buildFile = testProjectDir.newFile('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.root)
            .withArguments('helloWorld')
            .build()

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

实施任何自定义构建逻辑(如插件和任务类型)通常是一种惯例,该逻辑本质上比独立项目中的外部类复杂. 这种方法背后的主要驱动力是将编译后的代码捆绑到一个JAR文件中,将其发布到二进制存储库中,并在各个项目中重复使用.

Getting the plugin-under-test into the test build

GradleRunner使用Tooling API执行构建. 这意味着构建是在单独的过程中执行的(即,执行测试的过程不是同一过程). 因此,测试版本与测试过程不会共享相同的类路径或类加载器,并且测试代码也不能隐式地用于测试版本.

从2.13版本开始,Gradle提供了一种常规机制,可以将被测代码注入到测试版本中 .

对于Gradle的早期版本(2.13之前的版本),可以通过一些额外的配置来手动使被测代码可用. 下面的示例演示如何让生成的文件包含被测代码的实现类路径,并使其在测试运行时可用.

示例3.使测试类路径下的代码可用于测试
build.gradle
// Write the plugin's classpath to a file to share with the tests
task createClasspathManifest {
    def outputDir = file("$buildDir/$name")

    inputs.files(sourceSets.main.runtimeClasspath)
        .withPropertyName("runtimeClasspath")
        .withNormalizer(ClasspathNormalizer)
    outputs.dir(outputDir)
        .withPropertyName("outputDir")

    doLast {
        outputDir.mkdirs()
        file("$outputDir/plugin-classpath.txt").text = sourceSets.main.runtimeClasspath.join("\n")
    }
}

// Add the classpath file to the test runtime classpath
dependencies {
    testRuntimeOnly files(createClasspathManifest)
}
build.gradle.kts
// Write the plugin's classpath to a file to share with the tests
tasks.register("createClasspathManifest") {
    val outputDir = file("$buildDir/$name")

    inputs.files(sourceSets.main.get().runtimeClasspath)
        .withPropertyName("runtimeClasspath")
        .withNormalizer(ClasspathNormalizer::class)
    outputs.dir(outputDir)
        .withPropertyName("outputDir")

    doLast {
        outputDir.mkdirs()
        file("$outputDir/plugin-classpath.txt").writeText(sourceSets.main.get().runtimeClasspath.joinToString("\n"))
    }
}

// Add the classpath file to the test runtime classpath
dependencies {
    testRuntimeOnly(files(tasks["createClasspathManifest"]))
}
该示例的代码可以在Gradle的" -all"分发samples/testKit/gradleRunner/manualClasspathInjection中的samples/testKit/gradleRunner/manualClasspathInjection中找到.

然后,测试可以读取该值,并使用GradleRunner.withPluginClasspath(java.lang.Iterable)方法将类路径注入测试版本. 然后可以使用该类路径通过插件DSL在测试版本中定位插件(请参阅插件 ). 通过插件DSL应用插件需要定义插件标识符. 以下是在Spock Framework setup()方法中执行此操作的示例(在Groovy中),该方法类似于JUnit @Before方法.

Example: Injecting the code under test classes into test builds

src/test/groovy/org/gradle/sample/BuildLogicFunctionalTest.groovy
    List<File> pluginClasspath

    def setup() {
        settingsFile = testProjectDir.newFile('settings.gradle')
        buildFile = testProjectDir.newFile('build.gradle')

        def pluginClasspathResource = getClass().classLoader.findResource("plugin-classpath.txt")
        if (pluginClasspathResource == null) {
            throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.")
        }

        pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) }
    }

    def "hello world task prints hello world"() {
        given:
        buildFile << """
            plugins {
                id 'org.gradle.sample.helloworld'
            }
        """

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

        then:
        result.output.contains('Hello world!')
        result.task(":helloWorld").outcome == SUCCESS
    }
该示例的代码可以在Gradle的" -all"分发samples/testKit/gradleRunner/manualClasspathInjection中的samples/testKit/gradleRunner/manualClasspathInjection中找到.

当作为Gradle构建的一部分执行功能测试时,此方法效果很好. 从IDE执行功能测试时,还有一些额外的注意事项. 即,类路径清单文件指向Gradle而不是IDE生成的类文件等. 这意味着在更改被测代码的源代码之后,必须由Gradle重新编译源代码. 同样,如果被测代码的有效类路径发生变化,则必须重新生成清单. 无论哪种情况,执行构建的testClasses任务都将确保一切都是最新的.

一些IDE提供了一个方便的选项,可以将"测试类路径的生成和执行"委托给构建. 在IntelliJ中,您可以在``偏好设置...''>``构建,执行,部署''>``构建工具''>``Gradle''>``Runner''>``委派IDE生成/运行操作以进行共享''下找到此选项. 请查阅IDE的文档以获取更多信息.

Working with Gradle versions prior to 2.8

当使用早于2.8的Gradle版本执行构建时, GradleRunner.withPluginClasspath(java.lang.Iterable)方法将不起作用(请参阅用于测试的版本 ),因为在此类Gradle版本中不支持此功能.

相反,必须通过构建脚本本身注入代码. 下面的示例演示如何完成此操作.

Example: Injecting the code under test classes into test builds for Gradle versions prior to 2.8

src/test/groovy/org/gradle/sample/BuildLogicFunctionalTest.groovy
    List<File> pluginClasspath

    def setup() {
        settingsFile = testProjectDir.newFile('settings.gradle')
        buildFile = testProjectDir.newFile('build.gradle')

        def pluginClasspathResource = getClass().classLoader.findResource("plugin-classpath.txt")
        if (pluginClasspathResource == null) {
            throw new IllegalStateException("Did not find plugin classpath resource, run `testClasses` build task.")
        }

        pluginClasspath = pluginClasspathResource.readLines().collect { new File(it) }
    }

    def "hello world task prints hello world with pre Gradle 2.8"() {
        given:
        def classpathString = pluginClasspath
            .collect { it.absolutePath.replace('\\', '\\\\') } // escape backslashes in Windows paths
            .collect { "'$it'" }
            .join(", ")

        buildFile << """
            buildscript {
                dependencies {
                    classpath files($classpathString)
                }
            }
            apply plugin: "org.gradle.sample.helloworld"
        """

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

        then:
        result.output.contains('Hello world!')
        result.task(":helloWorld").outcome == SUCCESS
    }
该示例的代码可以在Gradle的" -all"分发samples/testKit/gradleRunner/manualClasspathInjection中的samples/testKit/gradleRunner/manualClasspathInjection中找到.

Automatic injection with the Java Gradle Plugin Development plugin

The Java Gradle Plugin development plugin can be used to assist in the development of Gradle plugins. Starting with Gradle version 2.13, the plugin provides a direct integration with TestKit. When applied to a project, the plugin automatically adds the gradleTestKit() dependency to the test compile configuration. Furthermore, it automatically generates the classpath for the code under test and injects it via GradleRunner.withPluginClasspath() for any GradleRunner instance created by the user. It’s important to note that the mechanism currently only works if the plugin under test is applied using the plugins DSL. If the target Gradle version is prior to 2.8, automatic plugin classpath injection is not performed.

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

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

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

这些约定中的任何一个都可以在类GradlePluginDevelopmentExtension的帮助下进行重新配置.

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

例子4.使用Java Gradle Development插件生成插件元数据
build.gradle
plugins {
    id 'groovy'
    id 'java-gradle-plugin'
}

dependencies {
    testImplementation('org.spockframework:spock-core:1.3-groovy-2.4') {
        exclude module: 'groovy-all'
    }
}
build.gradle.kts
plugins {
    groovy
    `java-gradle-plugin`
}

dependencies {
    testImplementation("org.spockframework:spock-core:1.3-groovy-2.4") {
        exclude(module = "groovy-all")
    }
}
该示例的代码可以在Gradle的" -all"分发版中的samples/testKit/gradleRunner/automaticClasspathInjectionQuickstart中找到.

Example: Automatically injecting the code under test classes into test builds

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.root)
        .withArguments('helloWorld')
        .withPluginClasspath()
        .build()

    then:
    result.output.contains('Hello world!')
    result.task(":helloWorld").outcome == SUCCESS
}
该示例的代码可以在Gradle的" -all"分发版中的samples/testKit/gradleRunner/automaticClasspathInjectionQuickstart中找到.

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

示例5.重新配置Java Gradle Development插件的类路径生成约定
build.gradle
plugins {
    id 'groovy'
    id 'java-gradle-plugin'
}

sourceSets {
    functionalTest {
        groovy {
            srcDir file('src/functionalTest/groovy')
        }
        resources {
            srcDir file('src/functionalTest/resources')
        }
        compileClasspath += sourceSets.main.output + configurations.testRuntimeClasspath
        runtimeClasspath += output + compileClasspath
    }
}

task functionalTest(type: Test) {
    testClassesDirs = sourceSets.functionalTest.output.classesDirs
    classpath = sourceSets.functionalTest.runtimeClasspath
}

check.dependsOn functionalTest

gradlePlugin {
    testSourceSets sourceSets.functionalTest
}

dependencies {
    functionalTestImplementation('org.spockframework:spock-core:1.3-groovy-2.4') {
        exclude module: 'groovy-all'
    }
}
build.gradle.kts
plugins {
    groovy
    `java-gradle-plugin`
}

sourceSets {
    create("functionalTest") {
        withConvention(GroovySourceSet::class) {
            groovy {
                srcDir(file("src/functionalTest/groovy"))
            }
        }
        resources {
            srcDir(file("src/functionalTest/resources"))
        }
        compileClasspath += sourceSets.main.get().output + configurations.testRuntimeClasspath
        runtimeClasspath += output + compileClasspath
    }
}

tasks.register<Test>("functionalTest") {
    testClassesDirs = sourceSets["functionalTest"].output.classesDirs
    classpath = sourceSets["functionalTest"].runtimeClasspath
}

tasks.check { dependsOn(tasks["functionalTest"]) }

gradlePlugin {
    testSourceSets(sourceSets["functionalTest"])
}

dependencies {
    "functionalTestImplementation"("org.spockframework:spock-core:1.3-groovy-2.4") {
        exclude(module = "groovy-all")
    }
}
该示例的代码可以在Gradle的" -all"分发版中的samples/testKit/gradleRunner/automaticClasspathInjectionCustomTestSourceSet中找到.

Controlling the build environment

运行程序通过在JVM的temp目录内的目录(即java.io.tmpdir系统属性指定的位置,通常为/tmp )中指定专用的"工作目录",在隔离的环境中执行测试构建. 默认的Gradle用户主目录中的任何配置(例如~/.gradle/gradle.properties )都不用于测试执行. TestKit并未提供对环境变量等进行细粒度控制的机制.TestKit的未来版本将提供改进的配置选项.

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

The Gradle version used to test

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

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

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

另外,可以通过以下任何GradleRunner方法指定要使用的Gradle的不同版本和特定版本:

它可以潜在地用于跨Gradle版本测试构建逻辑. 下面演示了编写为Groovy Spock测试的跨版本兼容性测试:

Example: Specifying a Gradle version for test execution

BuildLogicFunctionalTest.groovy
import org.gradle.testkit.runner.GradleRunner
import static org.gradle.testkit.runner.TaskOutcome.*
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification
import spock.lang.Unroll

class BuildLogicFunctionalTest extends Specification {
    @Rule final TemporaryFolder testProjectDir = new TemporaryFolder()
    File settingsFile
    File buildFile

    def setup() {
        settingsFile = testProjectDir.newFile('settings.gradle')
        buildFile = testProjectDir.newFile('build.gradle')
    }

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

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

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

        where:
        gradleVersion << ['2.6', '2.7']
    }
}

Feature support when testing with different Gradle versions

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

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

表1. Gradle版本兼容性
Feature 最低版本 Description

检查执行的任务

2.5

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

Plugin classpath injection

2.8

通过GradleRunner.withPluginClasspath(java.lang.Iterable)注入测试中的代码.

Inspecting build output in debug mode

2.9

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

Automatic plugin classpath injection

2.13

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

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

3.5

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

Debugging build logic

跑步者使用Tooling API执行构建. 这意味着构建是在单独的过程中执行的(即,执行测试的过程不是同一过程). 因此,以调试模式执行测试不允许您调试调试逻辑. 在IDE中设置的任何断点都不会因测试版本执行的代码而跳闸.

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

  • 使用 GradleRunner将JVM的" org.gradle.testkit.debug "系统属性设置为true (即,不是由运行程序执行的构建);

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

当需要启用调试支持而不对流道配置进行临时更改时,可以使用系统属性方法. 大多数IDE提供了设置JVM系统属性以执行测试的功能,并且可以使用此功能来设置此系统属性.

Testing with the Build Cache

To enable the Build Cache in your tests, you can pass the --build-cache argument to GradleRunner or use one of the other methods described in Enable the build cache. You can then check for the task outcome TaskOutcome.FROM_CACHE when your plugin’s custom task is cached. This outcome is only valid for Gradle 3.5 and newer.

Example: Testing cacheable tasks

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.root, 'build').deleteDir()
    result = runner()
        .withArguments( '--build-cache', 'cacheableTask')
        .build()

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

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

Example: Clean build cache between tests

BuildLogicFunctionalTest.groovy
@Rule final TemporaryFolder testProjectDir = new TemporaryFolder()
File buildFile
File localBuildCacheDirectory

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