測試在開發過程中扮演關鍵角色,能確保軟體可靠且品質高。此原則適用於建置程式碼,包括 Gradle 外掛。

範例專案

本節圍繞一個名為「URL 驗證器外掛」的範例專案。此外掛會建立一個名為 verifyUrl 的工作,用來檢查是否能透過 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

執行 verifyUrl 工作會顯示成功訊息,如果對設定的 URL 進行 HTTP GET 呼叫會傳回 200 回應碼

$ gradle verifyUrl

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

BUILD SUCCESSFUL in 0s
5 actionable tasks: 5 executed

在深入了解程式碼之前,讓我們先回顧不同的測試類型,以及支援實作這些測試的工具。

測試的重要性

測試是軟體開發生命週期中至關重要的一部分,可確保軟體在發佈前運作正常且符合品質標準。自動化測試讓開發人員可以放心地重構和改善程式碼。

測試金字塔

手動測試

雖然手動測試很直接,但容易出錯且需要人力。對於 Gradle 外掛,手動測試包括在建置指令碼中使用外掛。

自動化測試

自動化測試包含單元、整合和功能測試。

testing pyramid

Mike Cohen 在其著作 Succeeding with Agile: Software Development Using Scrum 中提出的測試金字塔描述了三種類型的自動化測試

  1. 單元測試:驗證最小的程式碼單元,通常是方法,並獨立執行。它使用 Stubs 或 Mocks 將程式碼與外部依賴項隔離。

  2. 整合測試:驗證多個單元或元件是否能一起運作。

  3. 功能測試:從最終使用者的角度測試系統,確保功能正確。Gradle 外掛的端對端測試會模擬建置、套用外掛,並執行特定任務以驗證功能。

工具支援

使用適當的工具,可以簡化手動和自動測試 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.gradle.kts
pluginManagement {
    includeBuild("../url-verifier-plugin")
}
settings.gradle
pluginManagement {
    includeBuild '../url-verifier-plugin'
}

來自專案 include-plugin-buildverifyUrl 任務的命令列輸出 看起來與簡介中顯示的完全相同,但現在它作為複合建置的一部分執行。

手動測試在開發過程中佔有一席之地,但它不能取代自動化測試。

設定自動化測試

在早期設定一組測試對外掛的成功至關重要。在將外掛升級到新的 Gradle 版本或加強/重構程式碼時,自動化測試會成為一個無價的安全網。

組織測試原始碼

我們建議實作單元、整合和功能測試的良好分配,以涵蓋最重要的使用案例。為每種類型的測試分開原始碼會自動產生一個更容易維護和管理的專案。

預設情況下,Java 專案會建立一個慣例,將單元測試組織在目錄 src/test/java 中。此外,如果您套用 Groovy 外掛程式,目錄 src/test/groovy 下的原始碼會被考慮編譯(目錄 src/test/kotlin 下的 Kotlin 標準相同)。因此,其他測試類型的原始碼目錄應遵循類似的模式

.
└── src
    ├── functionalTest
    │   └── groovy      (1)
    ├── integrationTest
    │   └── groovy      (2)
    ├── main
    │   ├── java        (3)
    └── test
        └── groovy      (4)
1 包含功能測試的原始碼目錄
2 包含整合測試的原始碼目錄
3 包含生產原始碼的原始碼目錄
4 包含單元測試的原始碼目錄
目錄 src/integrationTest/groovysrc/functionalTest/groovy 並非基於 Gradle 專案的現有標準慣例。您可以自由選擇最適合您的任何專案配置。

您可以設定編譯和測試執行的原始碼目錄。

測試套件外掛程式提供一個 DSL 和 API,將多個自動化測試群組建模成基於 JVM 的專案中的測試套件。您也可以依賴於第三方外掛程式以方便使用,例如 Nebula Facet 外掛程式TestSets 外掛程式

建模測試類型

一個新的建模以下 integrationTest 套件的組態 DSL 可透過孵化的 JVM 測試套件 外掛程式取得。

在 Gradle 中,原始碼目錄使用 原始碼組 的概念表示。原始碼組被設定為指向一個或多個包含原始碼的目錄。當您定義原始碼組時,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 相依性,它代表專案的「主要」變異(例如,編譯的外掛程式碼)

build.gradle.kts
val integrationTest by sourceSets.creating

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

dependencies {
    integrationTestImplementation(project)
}

來源組負責編譯原始碼,但不會執行位元組碼。對於測試執行,需要建立一個對應的 Test 類型的任務。以下設定顯示整合測試的執行,參考整合測試來源組的類別和執行時期類別路徑

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 測試架構,甚至包含用於建立 Stub 和 Mock 的 API。Gradle 團隊偏好 Spock,勝過其他選項,因為它具有表達性和簡潔性。

實作自動化測試

本節討論單元、整合和功能測試的代表性實作範例。所有測試類別都基於 Spock 的使用,儘管將程式碼調整到不同的測試架構應該相對容易。

實作單元測試

URL 驗證器外掛會發出 HTTP GET 呼叫,以檢查 URL 是否可以成功解析。方法 DefaultHttpCaller.get(String) 負責呼叫指定的 URL,並傳回 HttpResponse 類型的執行個體。HttpResponse 是包含 HTTP 回應代碼和訊息資訊的 POJO

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.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 測試。

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

Gradle 外掛程式的功能測試使用 GradleRunner 的執行個體來執行測試中的建置。GradleRunner 是 TestKit 提供的 API,它在內部使用 Tooling API 來執行建置。

以下範例將外掛程式套用至測試中的建置指令碼,設定擴充功能,並使用任務 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 任務來決定外掛程式類別路徑。您需要執行 assemble 任務來最初產生外掛程式類別路徑,或反映對它的變更,即使從 IDE 執行基於 TestKit 的功能測試也是如此。

某些 IDE 提供一個方便的選項,將「測試類別路徑產生和執行」委派給建置。在 IntelliJ 中,您可以在「偏好設定…​ > 建置、執行、部署 > 建置工具 > Gradle > 執行器 > 將 IDE 建置/執行動作委派給 Gradle」中找到這個選項。

intellij delegate to build