测试在开发过程中起着至关重要的作用,因为它确保可靠和高质量的软件。同样的原则适用于构建代码,更具体地说,适用于 Gradle 插件。在本节中,您将学习测试插件代码的有效技术。

本节假设您有:

示例项目

本节中的所有讨论都围绕一个名为 URL verifier plugin 的示例项目展开。该插件创建一个名为 verifyUrl 的任务,用于检查是否可以通过 HTTP GET 解析给定的 URL。最终用户可以通过名为 verification 的扩展名提供 URL。

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

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

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

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

$ gradle verifyUrl

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

BUILD SUCCESSFUL in 0s
5 actionable tasks: 5 executed

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

论测试的重要性

测试是软件开发生命周期中的一项基础活动。适当的测试可确保软件在发布给最终用户之前在功能和非功能级别上运行。作为副产品,自动化测试还使开发团队能够重构和改进代码,而不必担心在过程中引入回归。

测试金字塔

testing pyramid

测试软件最简单的方法可能是手动运行它。手动测试可以随时发生并且不受限于编写自动化代码。然而,手动测试容易出错且繁琐,因为它需要人员遍历一组预定义的测试用例。手动测试 Gradle 插件需要在构建脚本中使用插件二进制文件。

其他类型的测试可以完全自动化,并在每次更改源代码时进行测试。 Mike Cohen 在他的书 敏捷成功:使用 Scrum 进行软件开发 中介绍的测试金字塔描述了三种类型的自动化测试。

单元测试旨在验证最小的代码单元。在基于 Java 的项目中,这个单元是一个方法。单元测试通常不与系统的其他部分交互,例如数据库或文件系统。与系统其他部分的交互通常在存根或模拟的帮助下被切断。您会发现 POJO 和实用程序类是单元测试的良好候选者,因为它们是独立的并且不使用 Gradle API。

集成测试验证多个类或组件作为一个整体协同工作。被测代码可能会接触到外部子系统。

功能测试用于从最终用户的角度测试系统。 Gradle 插件的端到端测试建立构建脚本,应用被测插件并使用特定任务执行构建。构建的结果(例如标准输出/错误或生成的构件)验证功能的正确性。

工装支持

对 Gradle 插件实施手动和自动测试非常简单——它只需要正确的工具。下表简要概述了如何处理每种测试类型。请注意,您可以自由选择使用您最熟悉的测试框架。有关详细讨论和代码示例,请参阅下面的专用部分。

测试类型 工装支持

手动测试

Gradle 复合构建

单元测试

任何基于 JVM 的测试框架

集成测试

任何基于 JVM 的测试框架

功能测试

任何基于 JVM 的测试框架和 Gradle 测试套件

设置手动测试

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方法。

以下代码片段演示了设置文件的使用。

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

来自项目 include-plugin-build 看起来和介绍里的一模一样 的任务 verifyUrl 的命令行输出,除了它现在作为复合构建的一部分执行。

手动测试在开发过程中占有一席之地。它绝不是自动化测试的替代品。接下来,您将学习如何组织和实施 Gradle 插件的自动化测试。

设置自动化测试

尽早设置一套测试对于插件的成功至关重要。您将遇到各种情况,这些情况使您的测试成为您可以依赖的无价安全网,例如,将插件升级到新的 Gradle 版本以及增强或重构代码时。

整理测试源码

我们建议实施良好的单元、集成和功能测试分布,以涵盖最重要的用例。分离每种测试类型的源代码会自动生成一个更易于维护和管理的项目。

默认情况下,Java 项目已经创建了一个用于组织单元测试的约定,即目录 src/test/java 。此外,如果您应用 src/test/groovy 目录下的 Groovy 插件源代码,则会考虑进行编译。因此,其他测试类型的源代码目录应该遵循类似的模式。您可以在下面找到选择使用基于 Groovy 的测试方法的插件项目的示例项目布局。

.
└── src
    ├── functionalTest
    │   └── groovy      (1)
    ├── integrationTest
    │   └── groovy      (2)
    ├── main
    │   ├── java        (3)
    └── test
        └── groovy      (4)
1 包含功能测试的源目录
2 包含集成测试的源目录
3 包含生产源代码的源目录
4 包含单元测试的源目录
目录 src/integrationTest/groovysrc/functionalTest/groovy 不基于 Gradle 项目的现有标准约定。您可以自由选择最适合您的项目布局。

在下一节中,您将学习如何为编译和测试执行配置这些源目录。您还可以依靠第三方插件来获得便利,例如 Nebula Facet 插件测试集插件

建模测试类型

可通过 incubating JVM 测试套件 插件获得用于对以下 integrationTest 套件进行建模的新配置 DSL。

Gradle 在 源集概念 的帮助下对源代码目录进行建模。通过将源集的实例指向一个或多个源代码目录,Gradle will automatically create a corresponding compilation task out-of-the-box。可以使用一行构建脚本代码创建预配置的源集。源集自动注册configurations来为源集的源定义依赖关系。我们使用它来定义对项目本身的 integrationTestImplementation 依赖性,它代表我们项目的“主要”变体(即编译的插件代码)。

KotlinGroovy
build.gradle.kts
val integrationTest by sourceSets.creating

dependencies {
    "integrationTestImplementation"(project)
}

源集只负责编译源代码,不负责执行字节码。为了测试执行,需要建立相应的测试类型的任务。以下清单显示了执行集成测试的设置。正如您在下面看到的,该任务引用了集成测试源集的类和运行时类路径。

KotlinGroovy
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)
}

配置测试框架

Gradle 不规定使用特定的测试框架。流行的选择包括 JUnitTestNGSpock 。选择一个选项后,您必须将其依赖项添加到测试的编译类路径中。下面的代码片段展示了如何使用 Spock 来实现测试。我们选择将它用于所有三种测试类型(testintegrationTestfunctionalTest),从而为它们中的每一个定义一个依赖项。

KotlinGroovy
build.gradle.kts
repositories {
    mavenCentral()
}

dependencies {
    testImplementation(platform("org.spockframework:spock-bom:2.2-groovy-3.0"))
    testImplementation("org.spockframework:spock-core")

    "integrationTestImplementation"(platform("org.spockframework:spock-bom:2.2-groovy-3.0"))
    "integrationTestImplementation"("org.spockframework:spock-core")

    "functionalTestImplementation"(platform("org.spockframework:spock-bom:2.2-groovy-3.0"))
    "functionalTestImplementation"("org.spockframework:spock-core")
}

tasks.withType<Test>().configureEach {
    // Using JUnitPlatform for running tests
    useJUnitPlatform()
}
Spock 是一个基于 Groovy 的 BDD 测试框架,甚至包括用于创建 Stub 和 Mock 的 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 的类中提取尽可能多的逻辑,使其可作为单元测试进行测试。它将为您带来可维护的代码和更快的测试执行的好处。

实施集成测试

让我们看一下连接到另一个系统的类,即发出 HTTP 调用的代码段。在对类 DefaultHttpCaller 执行测试时,运行时环境需要能够连接到互联网。

DefaultHttpCaller.java
package org.myorg.http;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

public class DefaultHttpCaller implements HttpCaller {
    @Override
    public HttpResponse get(String url) {
        try {
            HttpURLConnection connection = (HttpURLConnection) new URL(url).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);
        }
    }
}

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 表示的插件实现的功能。如您所见,该实现公开了一个扩展和一个使用最终用户配置的 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 测试。

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

Gradle 插件的功能测试使用 GradleRunner 的实例来执行被测构建。 GradleRunner 是 TestKit 提供的 API,它在内部使用 Tooling API 来执行构建。以下示例将插件应用于正在测试的构建脚本,配置扩展并使用任务 verifyUrl 执行构建。请参阅 测试套件文档 以更熟悉 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
    }
}

集成开发环境

TestKit 通过运行特定的 Gradle 任务来确定插件类路径。即使在从 IDE 运行基于 TestKit 的功能测试时,您也需要执行 assemble 任务来初始生成插件类路径或反映对其的更改。

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

intellij delegate to build