使用 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 构建并检查结果。
可以创建一个人为的构建(例如以编程方式或从模板)来执行“被测试的逻辑”。然后可以以多种方式(例如任务和参数的不同组合)执行构建。然后可以通过断言以下内容(可能组合使用)来验证逻辑的正确性:
-
构建的输出;
-
构建的日志记录(即控制台输出);
-
构建执行的任务集及其结果(例如失败、最新等)。
创建并配置运行器实例后,可以根据预期结果通过GradleRunner.build()或GradleRunner.buildAndFail()方法执行构建。
下面演示了 Gradle runner 在 Java JUnit 测试中的用法:
示例:将 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 相比,它提供了许多引人注目的功能。
下面演示了Gradle runner在Groovy Spock测试中的用法:
示例:将 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 支持与 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 插件开发插件应用的标准约定自动注入插件类路径。
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 Plugin Development 插件提供的约定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'
}
控制构建环境
运行程序通过在 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 版本
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 Plugin Development 插件,通过GradleRunner.withPluginClasspath()自动注入被测代码。 |
|
设置构建要使用的环境变量。 |
3.5 |
Gradle Tooling API 仅支持在更高版本中设置环境变量。 |
调试构建逻辑
运行程序使用Tooling API来执行构建。这意味着构建是在单独的进程中执行的(即与执行测试的进程不同)。因此,在调试模式下执行测试不允许您按照预期调试构建逻辑。测试构建所执行的代码不会触发 IDE 中设置的任何断点。
TestKit 提供了两种不同的方式来启用调试模式:
-
使用JVM设置“
org.gradle.testkit.debug
”系统属性(即不是使用运行器执行构建);true
GradleRunner
当需要启用调试支持而不对运行器配置进行临时更改时,可以使用系统属性方法。大多数 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 User Home(请参阅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')
}