使用 TestKit 测试构建逻辑
Gradle TestKit(又名 TestKit)是一个库,旨在帮助测试 Gradle 插件和构建逻辑。目前,它专注于功能测试。也就是说,通过将其作为程序化执行构建的一部分来测试构建逻辑。随着时间的推移,TestKit 可能会扩展以促进其他类型的测试。
使用 TestKit
要使用 TestKit,请在插件的构建中包含以下内容
dependencies {
testImplementation(gradleTestKit())
}
dependencies {
testImplementation gradleTestKit()
}
gradleTestKit()
包含 TestKit 的类,以及 Gradle Tooling API 客户端。它不包含 JUnit、TestNG 或任何其他测试执行框架的版本。此类依赖项必须显式声明。
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
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 运行器
示例:将 GradleRunner 与 Java 和 JUnit 结合使用
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 运行器
示例:将 GradleRunner 与 Groovy 和 Spock 结合使用
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 执行构建。这意味着构建在单独的进程中执行(即,与执行测试的进程不同)。因此,测试构建不与测试进程共享相同的类路径或类加载器,并且待测代码对测试构建不是隐式可用的。
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 插件开发插件应用的标准约定来自动注入插件类路径。
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")
}
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'
}
示例:自动将待测代码类注入到测试构建中
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 测试套件插件,可以使用新的配置 DSL 对下面的 functionalTest 套件进行建模。 |
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")
}
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
系统属性;
设置用于测试的 Gradle 版本
Gradle 运行器需要 Gradle 发行版才能执行构建。TestKit 不依赖于 Gradle 的所有实现。
默认情况下,运行器将尝试根据加载 GradleRunner
类的位置查找 Gradle 发行版。也就是说,期望该类是从 Gradle 发行版加载的,就像使用 gradleTestKit()
依赖项声明时一样。
当将运行器用作由 Gradle 执行的测试的一部分时(例如,执行插件项目的 test
任务),运行器将使用用于执行测试的同一发行版。当将运行器用作由 IDE 执行的测试的一部分时,将使用导入项目时使用的相同 Gradle 发行版。这意味着插件实际上将使用与其构建版本相同的 Gradle 版本进行测试。
或者,可以通过以下任何 GradleRunner
方法指定要使用的不同且特定的 Gradle 版本
这可能用于跨 Gradle 版本测试构建逻辑。以下演示了一个作为 Groovy Spock 测试编写的跨版本兼容性测试
示例:为测试执行指定 Gradle 版本
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 版本敏感的功能。
功能 | 最低版本 | 描述 |
---|---|---|
检查已执行的任务 |
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(即,不是使用运行器执行的构建);
当希望在不临时更改运行器配置的情况下启用调试支持时,可以使用系统属性方法。大多数 IDE 都提供了为测试执行设置 JVM 系统属性的功能,并且可以使用此功能来设置此系统属性。
使用构建缓存进行测试
要在测试中启用构建缓存,您可以将 --build-cache
参数传递给 GradleRunner,或使用 启用构建缓存中描述的其他方法之一。然后,当插件的自定义任务被缓存时,您可以检查任务结果 TaskOutcome.FROM_CACHE。此结果仅对 Gradle 3.5 及更高版本有效。
示例:测试可缓存任务
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)),其中包含本地构建缓存的默认位置。为了使用构建缓存进行测试,应在测试之间清理构建缓存目录。实现此目的的最简单方法是将本地构建缓存配置为使用临时目录。
示例:在测试之间清理构建缓存
@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')
}