使用 TestKit 测试构建逻辑
Gradle TestKit(又称 TestKit)是一个库,有助于测试 Gradle 插件和构建逻辑。目前,它专注于功能测试。也就是说,通过在程序执行的构建中对其进行演练来测试构建逻辑。随着时间的推移,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 使用工具 API来执行构建。这意味着构建在单独的进程中执行(即不是执行测试的同一进程)。因此,测试构建与测试进程不共享相同的类路径或类加载器,并且待测代码不会隐式提供给测试构建。
GradleRunner 支持与工具 API 相同范围的 Gradle 版本。支持的版本在兼容性矩阵中定义。 使用较旧 Gradle 版本的构建可能仍然有效,但无法保证。 |
从 2.13 版开始,Gradle 提供了一种常规机制,可将待测代码注入测试构建中。
使用 Java Gradle 插件开发插件自动注入
Java Gradle 插件开发插件可用于协助 Gradle 插件的开发。从 Gradle 2.13 版开始,该插件提供了与 TestKit 的直接集成。应用于项目时,该插件会自动将gradleTestKit()
依赖项添加到testApi
配置中。此外,它会自动生成待测代码的类路径,并通过GradleRunner.withPluginClasspath()注入给用户创建的任何GradleRunner
实例。需要注意的是,该机制目前仅在使用插件 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
}
以下构建脚本演示了如何重新配置 Java Gradle 插件开发插件为使用自定义 Test
源代码集的项目提供的约定。
可以通过孵化中的 JVM 测试套件 插件获得用于对以下 functionalTest 套件建模的新配置 DSL。
|
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'
}
控制构建环境
Runner 在 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 仅支持在较新版本中设置环境变量。 |
调试构建逻辑
Runner 使用 Tooling API 执行构建。这意味着构建在单独的进程中执行(即不是执行测试的进程)。因此,在调试模式下执行测试不会让你像预期的那样调试构建逻辑。在 IDE 中设置的任何断点都不会被测试构建执行的代码触发。
TestKit 提供了两种不同的方式来启用调试模式
-
为使用
GradleRunner
的 JVM(即不是使用 Runner 执行的构建)设置“org.gradle.testkit.debug
”系统属性为true
;
当需要启用调试支持而不临时更改 Runner 配置时,可以使用系统属性方法。大多数 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')
}