使用 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 runner 进行功能测试
The GradleRunner 能够程序化地执行 Gradle 构建并检查结果。
可以创建一个人工构建(例如通过程序或模板创建),用于运行“被测逻辑”。然后可以执行该构建,可能通过多种方式(例如不同任务和参数的组合)。然后可以通过断言以下内容(可能组合使用)来验证逻辑的正确性:
-
构建的输出;
-
构建的日志(即控制台输出);
-
构建执行的任务集及其结果(例如 FAILED、UP-TO-DATE 等)。
在创建和配置 runner 实例后,可以根据预期结果通过 GradleRunner.build() 或 GradleRunner.buildAndFail() 方法执行构建。
以下演示了在 Java JUnit 测试中使用 Gradle runner 的方法:
示例:将 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 runner 的方法:
示例:将 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 执行构建。这意味着构建在一个单独的进程中执行(即与执行测试的进程不同)。因此,测试构建与测试进程不共享相同的 classpath 或类加载器,并且被测代码不会隐式地提供给测试构建。
GradleRunner 支持与 Tooling API 相同范围的 Gradle 版本。支持的版本在兼容性矩阵中定义。 使用旧版本 Gradle 的构建可能仍然有效,但不提供任何保证。 |
从 2.13 版本开始,Gradle 提供了一种惯例机制,用于将被测代码注入到测试构建中。
使用 Java Gradle 插件开发插件自动注入
The Java Gradle 插件开发插件可用于辅助 Gradle 插件的开发。从 Gradle 2.13 版本开始,该插件提供了与 TestKit 的直接集成。当应用于项目时,该插件会自动将 gradleTestKit()
依赖添加到 testApi
配置。此外,它会自动为被测代码生成 classpath,并通过 GradleRunner.withPluginClasspath() 方法将其注入到用户创建的任何 GradleRunner
实例中。重要的是要注意,该机制目前仅在被测插件使用 plugins DSL 应用时才有效。如果目标 Gradle 版本早于 2.8,则不执行自动插件 classpath 注入。
该插件在应用 TestKit 依赖和注入 classpath 时遵循以下约定:
-
包含被测代码的源集:
sourceSets.main
-
用于注入插件 classpath 的源集:
sourceSets.test
这些约定中的任何一个都可以借助 GradlePluginDevelopmentExtension 类重新配置。
以下基于 Groovy 的示例演示了如何使用 Java Gradle 插件开发插件应用的标准化约定来自动注入插件 classpath。
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 Test Suite 插件,可以使用新的配置 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'
}
控制构建环境
runner 通过在 JVM 临时目录(即由 java.io.tmpdir
系统属性指定的目录,通常是 /tmp
)内指定一个专用的“工作目录”来在隔离环境中执行测试构建。默认 Gradle 用户主目录中的任何配置(例如 ~/.gradle/gradle.properties
)都不会用于测试执行。TestKit 没有提供对环境所有方面(例如 JDK)进行细粒度控制的机制。TestKit 的未来版本将提供改进的配置选项。
TestKit 使用专用的守护进程,这些进程在测试执行后会自动关闭。
构建后,runner 不会删除专用的工作目录。TestKit 提供了两种方法来指定一个会定期清理的位置,例如项目的构建文件夹:
-
org.gradle.testkit.dir
系统属性;
设置用于测试的 Gradle 版本
Gradle runner 需要一个 Gradle 发行版才能执行构建。TestKit 不依赖于 Gradle 的所有实现。
默认情况下,runner 将尝试根据 GradleRunner
类加载的位置来查找 Gradle 发行版。也就是说,期望该类是从 Gradle 发行版加载的,就像使用 gradleTestKit()
依赖声明时一样。
当 runner 作为*由 Gradle 执行的*测试的一部分使用时(例如执行插件项目的 test
任务),runner 将使用与执行测试相同的发行版。当 runner 作为*由 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 及更高版本的构建。但是,某些 runner 特性在早期版本上不受支持。在这种情况下,尝试使用该特性时 runner 将抛出异常。
下表列出了对使用的 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 提供了两种不同的方法来启用调试模式:
-
将“org.gradle.testkit.debug”系统属性设置为
true
,作用于使用GradleRunner
的 JVM(即不是使用 runner 执行的构建);
当希望启用调试支持而无需临时更改 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')
}