测试在开发过程中扮演着至关重要的角色,确保软件可靠且高质量。这一原则同样适用于构建代码,包括 Gradle 插件。
示例项目
本节围绕一个名为“URL 验证器插件”的示例项目展开。该插件创建一个名为 verifyUrl
的任务,用于检查给定的 URL 是否可以通过 HTTP GET 解析。最终用户可以通过名为 verification
的扩展提供 URL。
以下构建脚本假设插件 JAR 文件已发布到二进制仓库。该脚本演示了如何将插件应用到项目并配置其公开的扩展。
plugins {
id("org.myorg.url-verifier") (1)
}
verification {
url = "https://www.google.com/" (2)
}
plugins {
id 'org.myorg.url-verifier' (1)
}
verification {
url = 'https://www.google.com/' (2)
}
1 | 将插件应用到项目 |
2 | 通过公开的扩展配置要验证的 URL |
如果对配置的 URL 的 HTTP GET 调用返回 200 响应码,执行 verifyUrl
任务将显示成功消息。
$ gradle verifyUrl
> Task :verifyUrl
Successfully resolved URL 'https://www.google.com/'
BUILD SUCCESSFUL in 0s
5 actionable tasks: 5 executed
在深入代码之前,让我们先回顾一下不同类型的测试以及支持实现这些测试的工具。
测试的重要性
测试是软件开发生命周期中至关重要的一部分,它确保软件在发布前能正常运行并符合质量标准。自动化测试使开发者能够自信地重构和改进代码。
测试金字塔
- 手动测试
-
虽然手动测试简单直接,但它容易出错并且需要人力。对于 Gradle 插件,手动测试涉及在构建脚本中使用插件。
- 自动化测试
-
自动化测试包括单元测试、集成测试和功能测试。

Mike Cohen 在他的著作《敏捷成功:使用 Scrum 进行软件开发》中介绍的测试金字塔描述了三种类型的自动化测试:
-
单元测试: 在隔离的环境下验证代码的最小单元,通常是方法。它使用 Stubs 或 Mocks 将代码与外部依赖隔离开来。
-
集成测试: 验证多个单元或组件是否协同工作。
-
功能测试: 从最终用户的角度测试系统,确保功能正确。Gradle 插件的端到端测试会模拟构建、应用插件并执行特定任务来验证功能。
工具支持
借助适当的工具,手动和自动化测试 Gradle 插件都变得简单。下表总结了每种测试方法。你可以选择任何你熟悉的测试框架。
有关详细说明和代码示例,请参阅下面的特定章节。
测试类型 | 工具支持 |
---|---|
任何基于 JVM 的测试框架 |
|
任何基于 JVM 的测试框架 |
|
任何基于 JVM 的测试框架和 Gradle TestKit |
设置手动测试
Gradle 的复合构建特性使得手动测试插件变得容易。独立的插件项目和消费项目可以合并成一个单元,这样就可以轻松地尝试或调试更改,而无需重新发布二进制文件。
. ├── include-plugin-build (1) │ ├── build.gradle │ └── settings.gradle └── url-verifier-plugin (2) ├── build.gradle ├── settings.gradle └── src
1 | 包含插件项目的消费项目 |
2 | 插件项目 |
有两种方法可以将插件项目包含到消费项目中:
-
通过使用命令行选项
--include-build
。 -
通过在
settings.gradle
中使用includeBuild
方法。
以下代码片段演示了 settings 文件的使用:
pluginManagement {
includeBuild("../url-verifier-plugin")
}
pluginManagement {
includeBuild '../url-verifier-plugin'
}
项目 include-plugin-build
的 verifyUrl
任务的命令行输出与引言中显示的完全相同,不同之处在于它现在作为复合构建的一部分执行。
手动测试在开发过程中占有一席之地,但它不能替代自动化测试。
设置自动化测试
及早建立一套测试对于插件的成功至关重要。在将插件升级到新的 Gradle 版本或增强/重构代码时,自动化测试成为宝贵的安全网。
组织测试源代码
我们建议合理地分配单元测试、集成测试和功能测试,以覆盖最重要的用例。分离每种测试类型的源代码可以使项目更易于维护和管理。
默认情况下,Java 项目会在目录 src/test/java
中组织单元测试。此外,如果你应用 Groovy 插件,目录 src/test/groovy
下的源代码也会被考虑编译(Kotlin 的标准相同,位于目录 src/test/kotlin
下)。因此,其他测试类型的源代码目录应遵循类似的模式:
. └── src ├── functionalTest │ └── groovy (1) ├── integrationTest │ └── groovy (2) ├── main │ ├── java (3) └── test └── groovy (4)
1 | 包含功能测试的源代码目录 |
2 | 包含集成测试的源代码目录 |
3 | 包含生产源代码的源代码目录 |
4 | 包含单元测试的源代码目录 |
目录 src/integrationTest/groovy 和 src/functionalTest/groovy 不基于 Gradle 项目的现有标准约定。你可以自由选择任何最适合你的项目布局。 |
你可以配置用于编译和测试执行的源代码目录。
测试套件插件 (Test Suite plugin) 提供了一个 DSL 和 API,用于在基于 JVM 的项目中将多组自动化测试建模为测试套件。你也可以依靠第三方插件来提供便利,例如 Nebula Facet 插件或 TestSets 插件。
建模测试类型
用于建模下方 integrationTest 套件的新配置 DSL 可通过孵化中的 JVM 测试套件 (JVM Test Suite) 插件获得。 |
在 Gradle 中,源代码目录使用源集 (source sets) 的概念来表示。源集被配置为指向一个或多个包含源代码的目录。当你定义一个源集时,Gradle 会自动为指定的目录设置编译任务。
可以用一行构建脚本代码创建一个预配置的源集。该源集会自动注册配置,以定义源集的源文件的依赖:
// Define a source set named 'test' for test sources
sourceSets {
test {
java {
srcDirs = ['src/test/java']
}
}
}
// Specify a test implementation dependency on JUnit
dependencies {
testImplementation 'junit:junit:4.12'
}
我们用它来定义一个 integrationTestImplementation
依赖指向项目本身,这代表了我们项目的“主”变体(即编译后的插件代码)。
val integrationTest by sourceSets.creating
dependencies {
"integrationTestImplementation"(project)
}
def integrationTest = sourceSets.create("integrationTest")
dependencies {
integrationTestImplementation(project)
}
源集负责编译源代码,但它们不处理字节码的执行。对于测试执行,需要建立一个相应类型为 Test 的任务。以下设置显示了集成测试的执行,引用了集成测试源集的类和运行时 classpath:
val integrationTestTask = tasks.register<Test>("integrationTest") {
description = "Runs the integration tests."
group = "verification"
testClassesDirs = integrationTest.output.classesDirs
classpath = integrationTest.runtimeClasspath
mustRunAfter(tasks.test)
}
tasks.check {
dependsOn(integrationTestTask)
}
def integrationTestTask = tasks.register("integrationTest", Test) {
description = 'Runs the integration tests.'
group = "verification"
testClassesDirs = integrationTest.output.classesDirs
classpath = integrationTest.runtimeClasspath
mustRunAfter(tasks.named('test'))
}
tasks.named('check') {
dependsOn(integrationTestTask)
}
配置测试框架
以下代码片段展示了如何使用 Spock 实现测试:
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform("org.spockframework:spock-bom:2.2-groovy-3.0"))
testImplementation("org.spockframework:spock-core")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
"integrationTestImplementation"(platform("org.spockframework:spock-bom:2.2-groovy-3.0"))
"integrationTestImplementation"("org.spockframework:spock-core")
"integrationTestRuntimeOnly"("org.junit.platform:junit-platform-launcher")
"functionalTestImplementation"(platform("org.spockframework:spock-bom:2.2-groovy-3.0"))
"functionalTestImplementation"("org.spockframework:spock-core")
"functionalTestRuntimeOnly"("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test>().configureEach {
// Using JUnitPlatform for running tests
useJUnitPlatform()
}
repositories {
mavenCentral()
}
dependencies {
testImplementation platform("org.spockframework:spock-bom:2.2-groovy-3.0")
testImplementation 'org.spockframework:spock-core'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
integrationTestImplementation platform("org.spockframework:spock-bom:2.2-groovy-3.0")
integrationTestImplementation 'org.spockframework:spock-core'
integrationTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
functionalTestImplementation platform("org.spockframework:spock-bom:2.2-groovy-3.0")
functionalTestImplementation 'org.spockframework:spock-core'
functionalTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.withType(Test).configureEach {
// Using JUnitPlatform for running tests
useJUnitPlatform()
}
Spock 是一个基于 Groovy 的 BDD 测试框架,甚至包含用于创建 Stubs 和 Mocks 的 API。Gradle 团队因其表达力和简洁性而偏好 Spock,而不是其他选项。 |
实现自动化测试
本节讨论了单元测试、集成测试和功能测试的典型实现示例。所有测试类都基于 Spock 的使用,尽管将代码适配到不同的测试框架应该相对容易。
实现单元测试
URL 验证器插件发出 HTTP GET 调用来检查 URL 是否可以成功解析。方法 DefaultHttpCaller.get(String)
负责调用给定的 URL 并返回一个 HttpResponse
类型的实例。 HttpResponse
是一个 POJO,包含有关 HTTP 响应码和消息的信息:
package org.myorg.http;
public class HttpResponse {
private int code;
private String message;
public HttpResponse(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
return "HTTP " + code + ", Reason: " + message;
}
}
类 HttpResponse
是进行单元测试的好选择。它不调用任何其他类,也不使用 Gradle API。
package org.myorg.http
import spock.lang.Specification
class HttpResponseTest extends Specification {
private static final int OK_HTTP_CODE = 200
private static final String OK_HTTP_MESSAGE = 'OK'
def "can access information"() {
when:
def httpResponse = new HttpResponse(OK_HTTP_CODE, OK_HTTP_MESSAGE)
then:
httpResponse.code == OK_HTTP_CODE
httpResponse.message == OK_HTTP_MESSAGE
}
def "can get String representation"() {
when:
def httpResponse = new HttpResponse(OK_HTTP_CODE, OK_HTTP_MESSAGE)
then:
httpResponse.toString() == "HTTP $OK_HTTP_CODE, Reason: $OK_HTTP_MESSAGE"
}
}
编写单元测试时,测试边界条件和各种形式的无效输入非常重要。尽量从使用 Gradle API 的类中提取逻辑,以便将其作为单元测试进行测试。这将使代码更易于维护并加快测试执行速度。 |
你可以使用 ProjectBuilder 类来创建 Project 实例,以便在测试插件实现时使用。
public class GreetingPluginTest {
@Test
public void greeterPluginAddsGreetingTaskToProject() {
Project project = ProjectBuilder.builder().build();
project.getPluginManager().apply("org.example.greeting");
assertTrue(project.getTasks().getByName("hello") instanceof GreetingTask);
}
}
实现集成测试
让我们看看一个调用其他系统的类,即发出 HTTP 调用的代码片段。在执行类 DefaultHttpCaller
的测试时,运行时环境需要能够访问互联网:
package org.myorg.http;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
public class DefaultHttpCaller implements HttpCaller {
@Override
public HttpResponse get(String url) {
try {
HttpURLConnection connection = (HttpURLConnection) new URI(url).toURL().openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
connection.connect();
int code = connection.getResponseCode();
String message = connection.getResponseMessage();
return new HttpResponse(code, message);
} catch (IOException e) {
throw new HttpCallException(String.format("Failed to call URL '%s' via HTTP GET", url), e);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
实现 DefaultHttpCaller
的集成测试与上一节所示的单元测试没有太大区别:
package org.myorg.http
import spock.lang.Specification
import spock.lang.Subject
class DefaultHttpCallerIntegrationTest extends Specification {
@Subject HttpCaller httpCaller = new DefaultHttpCaller()
def "can make successful HTTP GET call"() {
when:
def httpResponse = httpCaller.get('https://www.google.com/')
then:
httpResponse.code == 200
httpResponse.message == 'OK'
}
def "throws exception when calling unknown host via HTTP GET"() {
when:
httpCaller.get('https://www.wedonotknowyou123.com/')
then:
def t = thrown(HttpCallException)
t.message == "Failed to call URL 'https://www.wedonotknowyou123.com/' via HTTP GET"
t.cause instanceof UnknownHostException
}
}
实现功能测试
功能测试端到端地验证插件的正确性。实际上,这意味着应用、配置和执行插件实现的功能。UrlVerifierPlugin
类公开了一个扩展和一个任务实例,该任务实例使用最终用户配置的 URL 值:
package org.myorg;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.myorg.tasks.UrlVerify;
public class UrlVerifierPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
UrlVerifierExtension extension = project.getExtensions().create("verification", UrlVerifierExtension.class);
UrlVerify verifyUrlTask = project.getTasks().create("verifyUrl", UrlVerify.class);
verifyUrlTask.getUrl().set(extension.getUrl());
}
}
每个 Gradle 插件项目都应应用插件开发插件以减少样板代码。通过应用插件开发插件,测试源集被预配置为与 TestKit 一起使用。如果我们想为功能测试使用自定义源集,而将默认测试源集仅用于单元测试,我们可以配置插件开发插件在其他位置查找 TestKit 测试。
gradlePlugin {
testSourceSets(functionalTest)
}
gradlePlugin {
testSourceSets(sourceSets.functionalTest)
}
Gradle 插件的功能测试使用 GradleRunner
的实例来执行待测试的构建。 GradleRunner
是 TestKit 提供的一个 API,它在内部使用 Tooling API 来执行构建。
以下示例将插件应用到待测试的构建脚本,配置扩展并使用任务 verifyUrl
执行构建。请参阅 TestKit 文档以更熟悉 TestKit 的功能。
package org.myorg
import org.gradle.testkit.runner.GradleRunner
import spock.lang.Specification
import spock.lang.TempDir
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
class UrlVerifierPluginFunctionalTest extends Specification {
@TempDir File testProjectDir
File buildFile
def setup() {
buildFile = new File(testProjectDir, 'build.gradle')
buildFile << """
plugins {
id 'org.myorg.url-verifier'
}
"""
}
def "can successfully configure URL through extension and verify it"() {
buildFile << """
verification {
url = 'https://www.google.com/'
}
"""
when:
def result = GradleRunner.create()
.withProjectDir(testProjectDir)
.withArguments('verifyUrl')
.withPluginClasspath()
.build()
then:
result.output.contains("Successfully resolved URL 'https://www.google.com/'")
result.task(":verifyUrl").outcome == SUCCESS
}
}
IDE 集成
TestKit 通过运行特定的 Gradle 任务来确定插件 classpath。即使从 IDE 运行基于 TestKit 的功能测试,你也需要执行 assemble
任务来初步生成插件 classpath 或反映对其的更改。
一些 IDE 提供了便利选项,可以将“测试 classpath 生成和执行”委托给构建。在 IntelliJ 中,你可以在“Preferences… > Build, Execution, Deployment > Build Tools > Gradle > Runner > Delegate IDE build/run actions to Gradle”下找到此选项。
