测试在开发过程中起着至关重要的作用,通过确保可靠和高质量的软件。此原则适用于构建代码,包括 Gradle 插件。

示例项目

本节围绕一个名为“URL 验证器插件”的示例项目展开。此插件创建一个名为 verifyUrl 的 task,用于检查是否可以通过 HTTP GET 解析给定的 URL。最终用户可以通过名为 verification 的扩展提供 URL。

以下构建脚本假定插件 JAR 文件已发布到二进制仓库。该脚本演示了如何将插件应用于项目并配置其公开的扩展

build.gradle.kts
plugins {
    id("org.myorg.url-verifier")        (1)
}

verification {
    url = "https://www.google.com/"  (2)
}
build.gradle
plugins {
    id 'org.myorg.url-verifier'         (1)
}

verification {
    url = 'https://www.google.com/'     (2)
}
1 将插件应用于项目
2 通过公开的扩展配置要验证的 URL

如果对配置的 URL 的 HTTP GET 调用返回 200 响应代码,则执行 verifyUrl task 会呈现成功消息

$ gradle verifyUrl

> Task :verifyUrl
Successfully resolved URL 'https://www.google.com/'

BUILD SUCCESSFUL in 0s
5 actionable tasks: 5 executed

在深入研究代码之前,让我们先回顾一下不同类型的测试以及支持实现它们的工具。

测试的重要性

测试是软件开发生命周期的关键部分,确保软件在发布前功能正确并符合质量标准。自动化测试使开发人员能够自信地重构和改进代码。

测试金字塔

手动测试

虽然手动测试很简单,但它容易出错并且需要人工 effort。对于 Gradle 插件,手动测试涉及在构建脚本中使用插件。

自动化测试

自动化测试包括单元测试、集成测试和功能测试。

testing pyramid

Mike Cohen 在他的著作 Succeeding with Agile: Software Development Using Scrum 中介绍的测试金字塔描述了三种类型的自动化测试

  1. 单元测试: 验证最小的代码单元(通常是方法)的隔离性。它使用桩或模拟来隔离代码与外部依赖项。

  2. 集成测试: 验证多个单元或组件协同工作。

  3. 功能测试: 从最终用户的角度测试系统,确保功能正确。Gradle 插件的端到端测试模拟构建,应用插件,并执行特定的 task 以验证功能。

工具支持

借助适当的工具,可以简化 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 插件项目

有两种方法可以将插件项目包含在消费项目中

  1. 通过使用命令行选项 --include-build

  2. 通过在 settings.gradle 中使用方法 includeBuild

以下代码片段演示了 settings 文件的用法

settings.gradle.kts
pluginManagement {
    includeBuild("../url-verifier-plugin")
}
settings.gradle
pluginManagement {
    includeBuild '../url-verifier-plugin'
}

来自项目 include-plugin-buildverifyUrl task 的命令行输出 看起来与引言中显示的完全相同,只是它现在作为复合构建的一部分执行。

手动测试在开发过程中有其地位,但它不能替代自动化测试。

设置自动化测试

尽早设置测试套件对于插件的成功至关重要。当将插件升级到新的 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/groovysrc/functionalTest/groovy 不是基于 Gradle 项目的现有标准约定。您可以自由选择最适合您的项目布局。

您可以配置用于编译和测试执行的源目录。

Test Suite 插件 提供 DSL 和 API,用于将多组自动化测试建模到基于 JVM 的项目中的测试套件中。您还可以依靠第三方插件来获得便利,例如 Nebula Facet 插件TestSets 插件

建模测试类型

通过孵化中的 JVM Test Suite 插件,可以使用新的配置 DSL 来建模以下 integrationTest 套件。

在 Gradle 中,源代码目录使用 源集 的概念表示。源集配置为指向一个或多个包含源代码的目录。当您定义源集时,Gradle 会自动为指定的目录设置编译 task。

可以使用一行构建脚本代码创建预配置的源集。源集自动注册配置,以定义源集源的依赖项

// 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 依赖项,它代表我们项目的“main”变体(即,编译后的插件代码)

build.gradle.kts
val integrationTest by sourceSets.creating

dependencies {
    "integrationTestImplementation"(project)
}
build.gradle
def integrationTest = sourceSets.create("integrationTest")

dependencies {
    integrationTestImplementation(project)
}

源集负责编译源代码,但它们不处理执行字节码。对于测试执行,需要建立类型为 Test 的相应 task。以下设置显示了集成测试的执行,引用了集成测试源集的类和运行时类路径

build.gradle.kts
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)
}
build.gradle
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)
}

配置测试框架

Gradle 不指定使用特定的测试框架。流行的选择包括 JUnitTestNGSpock。选择一个选项后,您必须将其依赖项添加到测试的编译类路径中。

以下代码片段显示了如何使用 Spock 来实现测试

build.gradle.kts
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()
}
build.gradle
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 测试框架,甚至包括用于创建桩和模拟的 API。Gradle 团队因 Spock 的表达性和简洁性而更喜欢它而不是其他选项。

实现自动化测试

本节讨论单元测试、集成测试和功能测试的代表性实现示例。所有测试类都基于 Spock 的使用,尽管将代码调整为不同的测试框架应该相对容易。

实现单元测试

URL 验证器插件发出 HTTP GET 调用以检查是否可以成功解析 URL。方法 DefaultHttpCaller.get(String) 负责调用给定的 URL 并返回类型为 HttpResponse 的实例。HttpResponse 是一个 POJO,其中包含有关 HTTP 响应代码和消息的信息

HttpResponse.java
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。

HttpResponseTest.groovy
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 实例,以便在测试插件实现时使用。

src/test/java/org/example/GreetingPluginTest.java
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 的测试时,运行时环境需要能够访问互联网

DefaultHttpCaller.java
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 实现集成测试与上一节中显示的单元测试没有太大区别

DefaultHttpCallerIntegrationTest.groovy
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 类公开了一个扩展和一个 task 实例,该实例使用最终用户配置的 URL 值

UrlVerifierPlugin.java
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 测试。

build.gradle.kts
gradlePlugin {
    testSourceSets(functionalTest)
}
build.gradle
gradlePlugin {
    testSourceSets(sourceSets.functionalTest)
}

Gradle 插件的功能测试使用 GradleRunner 的实例来执行被测构建。GradleRunner 是 TestKit 提供的 API,它在内部使用 Tooling API 来执行构建。

以下示例将插件应用于被测构建脚本,配置扩展并使用 task verifyUrl 执行构建。请参阅 TestKit 文档 以更熟悉 TestKit 的功能。

UrlVerifierPluginFunctionalTest.groovy
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 task 来确定插件类路径。您需要执行 assemble task 以最初生成插件类路径,或者反映对其的更改,即使从 IDE 运行基于 TestKit 的功能测试时也是如此。

某些 IDE 提供了一个方便的选项,可以将“测试类路径生成和执行”委托给构建。在 IntelliJ 中,您可以在 Preferences…​ > Build, Execution, Deployment > Build Tools > Gradle > Runner > Delegate IDE build/run actions to Gradle 下找到此选项。

intellij delegate to build