JVM 上的測試是一個豐富的主題。有許多不同的測試函式庫和架構,以及許多不同類型的測試。所有這些都需要成為建置的一部分,無論它們執行頻率如何。本章致力於說明 Gradle 如何處理建置之間和建置內的不同需求,並重點說明它如何與兩種最常見的測試架構整合:JUnitTestNG

它說明

但首先,讓我們看看 Gradle 中 JVM 測試的基礎知識。

可透過孵化的 JVM 測試套件 外掛程式取得一個新的設定檔 DSL,用於建模測試執行階段。

基礎

所有 JVM 測試都圍繞著單一任務類型:測試。這會使用任何受支援的測試函式庫(JUnit、JUnit Platform 或 TestNG)執行一組測試案例,並整理結果。然後,您可以透過 測試報告 任務類型的執行個體將這些結果轉換成報告。

為了執行,測試任務類型只需要兩條資訊

當您使用 JVM 語言外掛程式(例如 Java 外掛程式)時,您將自動取得下列內容

  • 專用的 test 來源設定,用於單元測試

  • test 類型的 test 任務,用於執行那些單元測試

JVM 語言外掛程式使用來源設定,以適當的執行類別路徑和包含已編譯測試類別的目錄來設定任務。此外,它們會將 test 任務附加到 check 生命週期任務

另外,值得注意的是,test 來源設定會自動建立 對應的相依性設定,其中最有用的是 testImplementationtestRuntimeOnly,這些外掛程式會將它們繫結到 test 任務的類別路徑。

在大部分情況下,您只需要設定適當的編譯和執行時間相依性,並將任何必要的設定新增到 test 任務即可。下列範例顯示一個簡單的設定,它使用 JUnit Platform,並將測試的 JVM 最大堆積大小變更為 1 GB

build.gradle.kts
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.named<Test>("test") {
    useJUnitPlatform()

    maxHeapSize = "1G"

    testLogging {
        events("passed")
    }
}
build.gradle
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test', Test) {
    useJUnitPlatform()

    maxHeapSize = '1G'

    testLogging {
        events "passed"
    }
}

Test 任務有許多一般設定選項,以及數個架構特定的選項,您可以在 JUnitOptionsJUnitPlatformOptionsTestNGOptions 中找到它們的說明。我們會在本章節的其餘部分介紹其中的許多選項。

如果您想使用自己的測試類別設定自己的 Test 任務,最簡單的方法是建立您自己的來源設定和 Test 任務實例,如 設定整合測試 中所示。

測試執行

Gradle 會在一個獨立(「分岔」)的 JVM 中執行測試,與主要的建置程序隔離。這可防止類別路徑污染,以及建置程序過度消耗記憶體。它也允許您使用與建置不同的 JVM 參數來執行測試。

您可以透過 Test 任務上的多個屬性來控制測試程序的啟動方式,包括下列屬性

maxParallelForks — 預設值:1

你可以透過將此屬性設定為大於 1 的值來平行執行測試。這可能會讓你的測試套件完成得更快,特別是在多核心 CPU 上執行時。在使用平行測試執行時,請確保你的測試彼此適當地隔離。與檔案系統互動的測試特別容易發生衝突,導致間歇性測試失敗。

你的測試可以透過使用 org.gradle.test.worker 屬性的值來區分平行測試程序,每個程序的屬性值都是唯一的。你可以將其用於任何你想要的東西,但它特別適用於檔案名稱和其他資源識別碼,以防止我們剛才提到的衝突。

forkEvery — 預設值:0(無最大值)

此屬性指定 Gradle 在處置測試程序並建立新的測試程序之前,應該在測試程序上執行的最大測試類別數目。這主要是用於管理有記憶體洩漏的測試或框架,它們具有無法在測試之間清除或重設的靜態狀態。

警告:較低的值(0 除外)會嚴重損害測試效能

ignoreFailures — 預設值:false

如果此屬性為 true,則在測試完成後,Gradle 將繼續進行專案的建置,即使其中一些測試失敗也是如此。請注意,預設情況下,Test 任務會執行它偵測到的每個測試,而不考慮此設定。

failFast —(自 Gradle 4.6 起)預設值:false

如果你希望在其中一個測試失敗時,建置就會失敗並結束,請將此設定為 true。當你有一個執行時間長的測試套件時,這可以節省很多時間,特別是在持續整合伺服器上執行建置時。當建置在所有測試執行完畢之前失敗時,測試報告只會包含已完成測試的結果,無論是否成功。

你也可以使用 --fail-fast 命令列選項來啟用此行為,或分別使用 --no-fail-fast 來停用它。

testLogging — 預設值:未設定

此屬性表示一組選項,用於控制記錄哪些測試事件以及記錄在什麼層級。你也可以透過此屬性來設定其他記錄行為。有關更多詳細資訊,請參閱 TestLoggingContainer

dryRun — 預設值:false

如果此屬性為 true,Gradle 會模擬執行測試,而不會實際執行它們。這仍會產生報告,允許檢查已選取的測試。這可用於驗證您的測試篩選設定是否正確,而不會實際執行測試。

您也可以使用 --test-dry-run 命令列選項啟用此行為,或分別使用 --no-test-dry-run 停用此行為。

請參閱 測試,以取得所有可用設定選項的詳細資料。

如果設定不正確,測試程序可能會意外結束。例如,如果 Java 可執行檔不存在或提供了無效的 JVM 引數,測試程序將無法啟動。同樣地,如果測試對測試程序進行程式設計變更,這也可能導致意外失敗。

例如,如果在測試中修改了 SecurityManager,可能會發生問題,因為 Gradle 的內部訊息傳遞依賴於反射和 socket 通訊,如果安全性管理員的權限變更,可能會中斷這些傳遞。在此特定情況下,您應該在測試後還原原始的 SecurityManager,以便 gradle 測試工作程序可以繼續運作。

測試篩選

執行測試套件的子集是很常見的需求,例如當您正在修復錯誤或開發新的測試案例時。Gradle 提供了兩種機制來執行此操作

  • 篩選(建議選項)

  • 測試包含/排除

篩選取代包含/排除機制,但您仍然可以在實際應用中遇到後者。

使用 Gradle 的測試篩選,您可以根據下列條件選取要執行的測試

  • 完全限定的類別名稱或完全限定的方法名稱,例如 org.gradle.SomeTestorg.gradle.SomeTest.someMethod

  • 如果模式以大寫字母開頭,則為簡單的類別名稱或方法名稱,例如 SomeTestSomeTest.someMethod(自 Gradle 4.7 起)

  • '*' 萬用字元比對

您可以在建置指令碼中或透過 --tests 命令列選項啟用篩選。以下是每次執行建置時都會套用的某些篩選範例

build.gradle.kts
tasks.test {
    filter {
        //include specific method in any of the tests
        includeTestsMatching("*UiCheck")

        //include all tests from package
        includeTestsMatching("org.gradle.internal.*")

        //include all integration tests
        includeTestsMatching("*IntegTest")
    }
}
build.gradle
test {
    filter {
        //include specific method in any of the tests
        includeTestsMatching "*UiCheck"

        //include all tests from package
        includeTestsMatching "org.gradle.internal.*"

        //include all integration tests
        includeTestsMatching "*IntegTest"
    }
}

有關在建置指令碼中宣告篩選的更多詳細資料和範例,請參閱 TestFilter 參考。

命令列選項對於執行單一測試方法特別有用。當您使用 --tests 時,請注意建置指令碼中宣告的包含仍然會被採用。也可以提供多個 --tests 選項,其所有模式都會生效。下列各節有幾個使用命令列選項的範例。

並非所有測試架構都能順利使用篩選功能。有些進階的合成測試可能無法完全相容。不過,絕大多數的測試和使用案例都能完美地與 Gradle 的篩選機制搭配使用。

以下兩個區段將探討簡單類別/方法名稱和完全限定名稱的特定案例。

簡單名稱模式

自 4.7 版起,Gradle 會將以大寫字母開頭的模式視為簡單類別名稱或類別名稱 + 方法名稱。例如,下列命令列會執行 SomeTestClass 測試案例中的所有測試或其中一個測試,而與其位於哪個套件無關

# Executes all tests in SomeTestClass
gradle test --tests SomeTestClass

# Executes a single specified test in SomeTestClass
gradle test --tests SomeTestClass.someSpecificMethod

gradle test --tests SomeTestClass.*someMethod*

完全限定名稱模式

在 4.7 版之前,或如果模式並未以大寫字母開頭,Gradle 會將模式視為完全限定。因此,如果您想使用測試類別名稱而與其套件無關,您會使用 --tests *.SomeTestClass。以下是更多範例

# specific class
gradle test --tests org.gradle.SomeTestClass

# specific class and method
gradle test --tests org.gradle.SomeTestClass.someSpecificMethod

# method name containing spaces
gradle test --tests "org.gradle.SomeTestClass.some method containing spaces"

# all classes at specific package (recursively)
gradle test --tests 'all.in.specific.package*'

# specific method at specific package (recursively)
gradle test --tests 'all.in.specific.package*.someSpecificMethod'

gradle test --tests '*IntegTest'

gradle test --tests '*IntegTest*ui*'

gradle test --tests '*ParameterizedTest.foo*'

# the second iteration of a parameterized test
gradle test --tests '*ParameterizedTest.*[2]'

請注意,萬用字元 '*' 並不特別了解 '.' 套件分隔符號。它純粹是基於文字。因此,--tests *.SomeTestClass 會比對任何套件,而與其「深度」無關。

您也可以將在命令列中定義的篩選器與 持續建置 結合使用,以便在每次變更製作或測試原始檔後立即重新執行測試子集。下列範例會在每次變更觸發測試執行時,執行 'com.mypackage.foo' 套件或子套件中的所有測試

gradle test --continuous --tests "com.mypackage.foo.*"

測試報告

Test 任務預設會產生下列結果

  • HTML 測試報告

  • XML 測試結果,其格式與 Ant JUnit 報告任務相容,而後者則受到許多其他工具支援,例如 CI 伺服器

  • Test 任務用來產生其他格式的結果的有效率二進位格式

在大部分情況下,您會使用標準 HTML 報告,它會自動包含所有 Test 任務的結果,甚至包含您自己明確新增到建置中的任務。例如,如果您新增 Test 任務用於整合測試,如果兩個任務都執行,報告將包含單元測試和整合測試的結果。

如要彙總多個子專案的測試結果,請參閱 測試報告彙總外掛

與許多測試組態選項不同,有幾個專案層級 慣例屬性會影響測試報告。例如,您可以變更測試結果和報告的目的地,如下所示

build.gradle.kts
reporting.baseDir = file("my-reports")
java.testResultsDir = layout.buildDirectory.dir("my-test-results")

tasks.register("showDirs") {
    val rootDir = project.rootDir
    val reportsDir = project.reporting.baseDirectory
    val testResultsDir = project.java.testResultsDir

    doLast {
        logger.quiet(rootDir.toPath().relativize(reportsDir.get().asFile.toPath()).toString())
        logger.quiet(rootDir.toPath().relativize(testResultsDir.get().asFile.toPath()).toString())
    }
}
build.gradle
reporting.baseDir = "my-reports"
java.testResultsDir = layout.buildDirectory.dir("my-test-results")

tasks.register('showDirs') {
    def rootDir = project.rootDir
    def reportsDir = project.reporting.baseDirectory
    def testResultsDir = project.java.testResultsDir

    doLast {
        logger.quiet(rootDir.toPath().relativize(reportsDir.get().asFile.toPath()).toString())
        logger.quiet(rootDir.toPath().relativize(testResultsDir.get().asFile.toPath()).toString())
    }
}
gradle -q showDirs 的輸出
> gradle -q showDirs
my-reports
build/my-test-results

請追蹤連結至慣例屬性以取得更多詳細資料。

還有一個獨立的 TestReport 任務類型,可用於產生自訂的 HTML 測試報告。它只需要 destinationDir 的值和要包含在報告中的測試結果。以下是一個範例,它會為所有子專案的單元測試產生一個合併的報告

buildSrc/src/main/kotlin/myproject.java-conventions.gradle.kts
plugins {
    id("java")
}

// Disable the test report for the individual test task
tasks.named<Test>("test") {
    reports.html.required = false
}

// Share the test report data to be aggregated for the whole project
configurations.create("binaryTestResultsElements") {
    isCanBeResolved = false
    isCanBeConsumed = true
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("test-report-data"))
    }
    outgoing.artifact(tasks.test.map { task -> task.getBinaryResultsDirectory().get() })
}
build.gradle.kts
val testReportData by configurations.creating {
    isCanBeConsumed = false
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
        attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("test-report-data"))
    }
}

dependencies {
    testReportData(project(":core"))
    testReportData(project(":util"))
}

tasks.register<TestReport>("testReport") {
    destinationDirectory = reporting.baseDirectory.dir("allTests")
    // Use test results from testReportData configuration
    testResults.from(testReportData)
}
buildSrc/src/main/groovy/myproject.java-conventions.gradle
plugins {
    id 'java'
}

// Disable the test report for the individual test task
test {
    reports.html.required = false
}

// Share the test report data to be aggregated for the whole project
configurations {
    binaryTestResultsElements {
        canBeResolved = false
        canBeConsumed = true
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
            attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 'test-report-data'))
        }
        outgoing.artifact(test.binaryResultsDirectory)
    }
}
build.gradle
// A resolvable configuration to collect test reports data
configurations {
    testReportData {
        canBeConsumed = false
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
            attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 'test-report-data'))
        }
    }
}

dependencies {
    testReportData project(':core')
    testReportData project(':util')
}

tasks.register('testReport', TestReport) {
    destinationDirectory = reporting.baseDirectory.dir('allTests')
    // Use test results from testReportData configuration
    testResults.from(configurations.testReportData)
}

在此範例中,我們使用慣例外掛程式 myproject.java-conventions 來將專案的測試結果公開給 Gradle 的 變異感知相依性管理引擎

外掛程式宣告一個可消耗的 binaryTestResultsElements 組態,它代表 test 任務的二進位測試結果。在聚合專案的建置檔案中,我們宣告 testReportData 組態,並相依於我們要聚合其結果的所有專案。Gradle 會自動從每個子專案中選取二進位測試結果變異,而不是專案的 JAR 檔案。最後,我們新增一個 testReport 任務,它會聚合來自 testResultsDirs 屬性的測試結果,其中包含從 testReportData 組態解析的所有二進位測試結果。

您應該注意,TestReport 類型會結合多個測試任務的結果,並需要聚合個別測試類別的結果。這表示如果一個給定的測試類別是由多個測試任務執行的,那麼測試報告將包含該類別的執行,但要區分該類別的個別執行及其輸出可能很困難。

透過 XML 檔案將測試結果傳達給 CI 伺服器和其他工具

測試任務會建立描述測試結果的 XML 檔案,採用「JUnit XML」的偽標準。CI 伺服器和其他工具通常會透過這些 XML 檔案觀察測試結果。

預設情況下,這些檔案會寫入 layout.buildDirectory.dir("test-results/$testTaskName"),每個測試類別一個檔案。可以變更專案中所有測試任務的位置,或針對每個測試任務個別變更。

build.gradle.kts
java.testResultsDir = layout.buildDirectory.dir("junit-xml")
build.gradle
java.testResultsDir = layout.buildDirectory.dir("junit-xml")

使用上述組態後,XML 檔案會寫入 layout.buildDirectory.dir("junit-xml/$testTaskName")

build.gradle.kts
tasks.test {
    reports {
        junitXml.outputLocation = layout.buildDirectory.dir("test-junit-xml")
    }
}
build.gradle
test {
    reports {
        junitXml.outputLocation = layout.buildDirectory.dir("test-junit-xml")
    }
}

使用上述設定,test 任務的 XML 檔案會寫入 layout.buildDirectory.dir("test-results/test-junit-xml")。其他測試任務的 XML 檔案位置會保持不變。

設定選項

XML 檔案的內容也可以透過設定 JUnitXmlReport 選項,以不同的方式傳達結果。

build.gradle.kts
tasks.test {
    reports {
        junitXml.apply {
            isOutputPerTestCase = true // defaults to false
            mergeReruns = true // defaults to false
        }
    }
}
build.gradle
test {
    reports {
        junitXml {
            outputPerTestCase = true // defaults to false
            mergeReruns = true // defaults to false
        }
    }
}
outputPerTestCase

啟用 outputPerTestCase 選項時,會將測試案例期間產生的任何輸出記錄與結果中的該測試案例關聯。停用時(預設值),輸出會與整個測試類別關聯,而不是產生記錄輸出的個別測試案例(例如測試方法)。大多數觀察 JUnit XML 檔案的現代工具都支援「每個測試案例的輸出」格式。

如果您使用 XML 檔案來傳達測試結果,建議啟用此選項,因為它提供更有用的報告。

mergeReruns

啟用 mergeReruns 時,如果測試失敗但重試後成功,其失敗會記錄為 <flakyFailure>,而不是 <failure>,在一個 <testcase> 內。這實際上是啟用重試時 Apache Maven™ 的 surefire 外掛 產生的報告。如果您的 CI 伺服器了解此格式,它會指出測試不穩定。如果它不了解,它會指出測試成功,因為它會忽略 <flakyFailure> 資訊。如果測試沒有成功(即每次重試都失敗),它會指出測試失敗,無論您的工具是否了解此格式。

停用 mergeReruns(預設值)時,每次執行測試都會列為一個單獨的測試案例。

如果您使用 建置掃描Develocity,不穩定的測試會在不考慮此設定的情況下被偵測出來。

啟用此選項在使用 CI 工具時特別有用,該工具使用 XML 測試結果來判斷建置失敗,而不是依賴 Gradle 來判斷建置是否失敗,而當所有失敗的測試在重試時都通過時,您希望不將建置視為失敗。Jenkins CI 伺服器及其JUnit 外掛程式就是這種情況。啟用 mergeReruns 後,在重試時通過的測試將不再導致此 Jenkins 外掛程式將建置視為失敗。然而,失敗的測試執行將從 Jenkins 測試結果視覺化中省略,因為它不考慮 <flakyFailure> 資訊。除了 JUnit Jenkins 外掛程式外,還可以另外使用Flaky Test Handler Jenkins 外掛程式,以視覺化這些「不穩定的失敗」。

測試會根據其報告的名稱進行分組和合併。在使用任何會影響報告的測試名稱的測試參數化,或任何其他會產生潛在動態測試名稱的機制時,應注意確保測試名稱穩定且不會不必要地變更。

啟用 mergeReruns 選項不會為測試執行新增任何重試/重新執行功能。可以透過測試執行架構(例如 JUnit 的@RepeatedTest)或透過Test Retry Gradle 外掛程式來啟用重新執行。

測試偵測

預設情況下,Gradle 會執行它偵測到的所有測試,它會透過檢查已編譯的測試類別來執行此動作。此偵測會根據所使用的測試架構使用不同的準則。

對於JUnit,Gradle 會掃描 JUnit 3 和 4 測試類別。如果類別符合下列條件,則會將其視為 JUnit 測試

  • 最終繼承自 TestCaseGroovyTestCase

  • 加上 @RunWith 註解

  • 包含加上 @Test 註解的方法,或其超類別有加上此註解

對於TestNG,Gradle 會掃描加上 @Test 註解的方法。

請注意不會執行抽象類別。此外,請注意 Gradle 會掃描測試類別路徑上的 JAR 檔案中的繼承樹狀結構。因此,如果那些 JAR 檔案包含測試類別,它們也會被執行。

如果您不想使用測試類別偵測,您可以透過將Test上的 scanForTestClasses 屬性設定為 false 來停用它。當您這麼做時,測試工作只會使用 includesexcludes 屬性來尋找測試類別。

如果 scanForTestClasses 為 false,且未指定任何包含或排除模式,Gradle 預設會執行符合 **/*Tests.class**/*Test.class 模式的任何類別,排除符合 **/Abstract*.class 的類別。

使用JUnit Platform時,只會使用 includesexcludes 來篩選測試類別 — scanForTestClasses 沒有作用。

測試記錄

Gradle 允許對記錄到主控台的事件進行微調控制。記錄可以根據每個記錄等級進行設定,而預設會記錄下列事件

當記錄等級為

記錄的事件

其他設定

ERRORQUIETWARNING

LIFECYCLE

測試失敗

例外狀況格式為 SHORT

INFO

測試失敗略過的測試測試標準輸出測試標準錯誤

堆疊追蹤已截斷。

DEBUG

所有事件

記錄完整的堆疊追蹤。

測試記錄可以根據每個記錄等級進行修改,方法是調整測試任務的 testLogging 屬性中適當的 TestLogging 執行個體。例如,若要調整 INFO 等級的測試記錄設定,請修改 TestLoggingContainer.getInfo() 屬性。

測試分組

JUnit、JUnit Platform 和 TestNG 允許對測試方法進行精密的分類。

此部分適用於將具有相同測試目的(單元測試、整合測試、驗收測試等)的測試集合中的個別測試類別或方法進行分組。若要根據測試類別的目的進行區分,請參閱孵化的 JVM 測試套件 外掛程式。

JUnit 4.8 引入了類別概念,用於將 JUnit 4 測試類別和方法分組。[1] Test.useJUnit(org.gradle.api.Action) 允許您指定要包含和排除的 JUnit 類別。例如,下列設定會包含 CategoryA 中的測試,並排除 test 任務中 CategoryB 中的測試

範例 8. JUnit 類別
build.gradle.kts
tasks.test {
    useJUnit {
        includeCategories("org.gradle.junit.CategoryA")
        excludeCategories("org.gradle.junit.CategoryB")
    }
}
build.gradle
test {
    useJUnit {
        includeCategories 'org.gradle.junit.CategoryA'
        excludeCategories 'org.gradle.junit.CategoryB'
    }
}

JUnit Platform 引入了 標籤 來取代類別。您可以透過 Test.useJUnitPlatform(org.gradle.api.Action) 指定包含/排除的標籤,如下所示

build.gradle.kts
tasks.withType<Test>().configureEach {
    useJUnitPlatform {
        includeTags("fast")
        excludeTags("slow")
    }
}
build.gradle
tasks.withType(Test).configureEach {
    useJUnitPlatform {
        includeTags 'fast'
        excludeTags 'slow'
    }
}

TestNG 架構使用測試群組概念來產生類似效果。[2] 您可以透過 Test.useTestNG(org.gradle.api.Action) 設定在測試執行期間包含或排除哪些測試群組,如下所示

build.gradle.kts
tasks.named<Test>("test") {
    useTestNG {
        val options = this as TestNGOptions
        options.excludeGroups("integrationTests")
        options.includeGroups("unitTests")
    }
}
build.gradle
test {
    useTestNG {
        excludeGroups 'integrationTests'
        includeGroups 'unitTests'
    }
}

使用 JUnit 5

JUnit 5 是著名的 JUnit 測試架構的最新版本。與其前身不同,JUnit 5 是模組化的,並由多個模組組成

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform 可作為在 JVM 上啟動測試架構的基礎。JUnit Jupiter 是新的 程式設計模型擴充功能模型 的組合,用於在 JUnit 5 中撰寫測試和擴充功能。JUnit Vintage 提供一個 TestEngine,用於在平台上執行基於 JUnit 3 和 JUnit 4 的測試。

下列程式碼在 build.gradle 中啟用 JUnit Platform 支援

build.gradle.kts
tasks.named<Test>("test") {
    useJUnitPlatform()
}
build.gradle
tasks.named('test', Test) {
    useJUnitPlatform()
}

請參閱 Test.useJUnitPlatform() 以取得更多詳細資料。

編譯和執行 JUnit Jupiter 測試

要在 Gradle 中啟用 JUnit Jupiter 支援,您只需新增以下相依性

build.gradle.kts
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
build.gradle
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

然後,您可以將測試案例放入 src/test/java 中,並使用 gradle test 執行它們。

使用 JUnit Vintage 執行舊版測試

如果您要在 JUnit Platform 上執行 JUnit 3/4 測試,甚至將它們與 Jupiter 測試混合,則應新增額外的 JUnit Vintage Engine 相依性

build.gradle.kts
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    testCompileOnly("junit:junit:4.13")
    testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
build.gradle
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
    testCompileOnly 'junit:junit:4.13'
    testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

這樣一來,您就可以使用 gradle test 在 JUnit Platform 上測試 JUnit 3/4 測試,而無需重新編寫它們。

過濾測試引擎

JUnit Platform 允許您使用不同的測試引擎。JUnit 目前提供兩個現成的 TestEngine 實作:junit-jupiter-enginejunit-vintage-engine。您也可以撰寫並插入您自己的 TestEngine 實作,如 此處 所述。

預設情況下,將使用測試執行時間類別路徑上的所有測試引擎。若要明確控制特定的測試引擎實作,您可以將以下設定新增到您的建置指令碼

build.gradle.kts
tasks.withType<Test>().configureEach {
    useJUnitPlatform {
        includeEngines("junit-vintage")
        // excludeEngines("junit-jupiter")
    }
}
build.gradle
tasks.withType(Test).configureEach {
    useJUnitPlatform {
        includeEngines 'junit-vintage'
        // excludeEngines 'junit-jupiter'
    }
}

TestNG 中的測試執行順序

當您使用 testng.xml 檔案時,TestNG 允許明確控制測試的執行順序。沒有此類檔案(或由 TestNGOptions.getSuiteXmlBuilder() 設定的等效檔案),您無法指定測試執行順序。但是,您可以控制測試的所有面向(包括其相關的 @BeforeXXX@AfterXXX 方法,例如註解為 @Before/AfterClass@Before/AfterMethod 的方法)是否在開始下一個測試之前執行。您透過將 TestNGOptions.getPreserveOrder() 屬性設定為 true 來執行此操作。如果您將其設定為 false,您可能會遇到執行順序類似於:TestA.doBeforeClass()TestB.doBeforeClass()TestA 測試的情況。

直接使用 testng.xml 檔案時,保留測試順序是預設行為,但 Gradle 的 TestNG 整合所使用的 TestNG API 預設會以無法預測的順序執行測試。[3]保留測試執行順序的功能是在 TestNG 版本 5.14.5 中引入的。如果將舊版 TestNG 的 preserveOrder 屬性設定為 true,組建會失敗。

build.gradle.kts
tasks.test {
    useTestNG {
        preserveOrder = true
    }
}
build.gradle
test {
    useTestNG {
        preserveOrder true
    }
}

groupByInstance 屬性控制是否要依據執行個體而非類別來分組測試。 TestNG 文件 更詳細地說明了差異,但基本上,如果您有一個測試方法 A() 取決於 B(),依據執行個體分組可確保每個 A-B 配對(例如 B(1)-A(1))在下一組配對之前執行。依據類別分組時,會先執行所有 B() 方法,然後再執行所有 A() 方法。

請注意,通常只有在使用資料提供者來參數化測試時,才會有多個測試執行個體。此外,依據執行個體分組測試的功能是在 TestNG 版本 6.1 中引入的。如果將舊版 TestNG 的 groupByInstances 屬性設定為 true,組建會失敗。

build.gradle.kts
tasks.test {
    useTestNG {
        groupByInstances = true
    }
}
build.gradle
test {
    useTestNG {
        groupByInstances = true
    }
}

TestNG 參數化方法和報告

TestNG 支援 參數化測試方法,允許使用不同的輸入多次執行特定測試方法。Gradle 會在測試方法執行報告中包含參數值。

假設有一個名為 aTestMethod 的參數化測試方法,它需要兩個參數,它會以 aTestMethod(toStringValueOfParam1, toStringValueOfParam2) 的名稱報告。這有助於識別特定反覆運算的參數值。

設定整合測試

專案的常見需求是以某種形式納入整合測試。其目的是驗證專案的各個部分是否正常運作。這通常表示它們需要特殊的執行設定和相依性,與單元測試不同。

將整合測試新增到組建的最簡單方法是利用正在孵化的 JVM 測試套件 外掛程式。如果孵化的解決方案不適合您,以下是您在組建中需要執行的步驟

  1. 為它們建立一個新的來源組

  2. 將您需要的相依性加入該來源組的適當組態中

  3. 為該來源組設定編譯和執行時期類別路徑

  4. 建立一個工作來執行整合測試

您可能還需要根據整合測試的形式執行一些額外的設定。我們將在進行時討論這些設定。

讓我們從一個實務範例開始,在一個建置指令碼中實作前三個步驟,並以一個新的來源組intTest為中心

build.gradle.kts
sourceSets {
    create("intTest") {
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
}

val intTestImplementation by configurations.getting {
    extendsFrom(configurations.implementation.get())
}
val intTestRuntimeOnly by configurations.getting

configurations["intTestRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get())

dependencies {
    intTestImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
    intTestRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
build.gradle
sourceSets {
    intTest {
        compileClasspath += sourceSets.main.output
        runtimeClasspath += sourceSets.main.output
    }
}

configurations {
    intTestImplementation.extendsFrom implementation
    intTestRuntimeOnly.extendsFrom runtimeOnly
}

dependencies {
    intTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
    intTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

這將設定一個新的來源組,稱為intTest,它會自動建立

  • intTestImplementationintTestCompileOnlyintTestRuntimeOnly組態(以及一些較不常用的組態

  • 一個compileIntTestJava工作,它將編譯src/intTest/java下的所有來源檔案

如果您使用 IntelliJ IDE,您可能希望將這些額外來源組中的目錄標記為包含測試來源,而不是生產來源,如Idea 外掛程式文件所述。

範例也執行下列動作,您可能不需要所有這些動作來進行您的特定整合測試

  • main來源組中的生產類別加入整合測試的編譯和執行時期類別路徑中 — sourceSets.main.output是一個檔案集合,其中包含所有目錄,這些目錄包含已編譯的生產類別和資源

  • intTestImplementation組態從implementation延伸,這表示生產程式碼的所有宣告相依性也變成整合測試的相依性

  • intTestRuntimeOnly組態執行相同的動作

在大多數情況下,您希望您的整合測試可以存取受測類別,這就是為什麼我們在這個範例中確保這些類別包含在編譯和執行時期類別路徑中的原因。但是,有些類型的測試會以不同的方式與生產程式碼互動。例如,您可能有測試將您的應用程式作為可執行檔執行,並驗證輸出。在 Web 應用程式的案例中,測試可能會透過 HTTP 與您的應用程式互動。由於在這種情況下,測試不需要直接存取受測類別,因此您不需要將生產類別加入測試類別路徑中。

另一個常見步驟是將所有單元測試相依性也附加到整合測試中 — 透過intTestImplementation.extendsFrom testImplementation — 但這只有在整合測試需要所有或幾乎所有與單元測試相同的相依性時才有意義。

您應該注意範例中的其他幾個面向

  • +=允許您將路徑和路徑集合附加到compileClasspathruntimeClasspath,而不是覆寫它們

  • 如果您想要使用基於慣例的設定,例如 intTestImplementation,您必須在新的來源組後宣告依賴項

建立並設定來源組會自動設定編譯階段,但對於執行整合測試並無作用。因此,最後一塊拼圖是一個自訂測試任務,它使用新來源組中的資訊來設定其執行時期類別路徑和測試類別

build.gradle.kts
val integrationTest = task<Test>("integrationTest") {
    description = "Runs integration tests."
    group = "verification"

    testClassesDirs = sourceSets["intTest"].output.classesDirs
    classpath = sourceSets["intTest"].runtimeClasspath
    shouldRunAfter("test")

    useJUnitPlatform()

    testLogging {
        events("passed")
    }
}

tasks.check { dependsOn(integrationTest) }
build.gradle
tasks.register('integrationTest', Test) {
    description = 'Runs integration tests.'
    group = 'verification'

    testClassesDirs = sourceSets.intTest.output.classesDirs
    classpath = sourceSets.intTest.runtimeClasspath
    shouldRunAfter test

    useJUnitPlatform()

    testLogging {
        events "passed"
    }
}

check.dependsOn integrationTest

我們再次存取來源組以取得相關資訊,例如已編譯測試類別的位置(testClassesDirs 屬性)以及執行它們時類別路徑上需要什麼(classpath)。

使用者通常希望在單元測試後執行整合測試,因為它們執行起來通常較慢,而且您希望建置在單元測試上盡早失敗,而不是在整合測試上較晚失敗。這就是上述範例新增 shouldRunAfter() 宣告的原因。這比 mustRunAfter() 更受青睞,因此 Gradle 在平行執行建置時有更大的彈性。

有關如何判斷其他來源組中測試的程式碼覆蓋率的資訊,請參閱 JaCoCo 外掛程式JaCoCo 報告彙總外掛程式 章節。

測試 Java 模組

如果您正在 開發 Java 模組,本章節中描述的所有內容仍然適用,而且可以使用任何受支援的測試架構。不過,有一些事項需要考量,具體取決於您是否需要在測試執行期間提供模組資訊,以及是否需要強制執行模組界線。在此脈絡中,術語白盒測試(停用或放寬模組界線)和黑盒測試(模組界線就緒)經常被使用。白盒測試用於/需要單元測試,而黑盒測試則符合功能或整合測試需求。

類別路徑上的白盒單元測試執行

撰寫模組中函式或類別的單元測試最簡單的設定是不要在測試執行期間使用模組特定資訊。為此,您只需以撰寫一般函式庫的方式撰寫測試即可。如果您的測試原始碼設定 (src/test/java) 中沒有 module-info.java 檔案,此原始碼設定會在編譯和測試執行期間視為傳統 Java 函式庫。這表示所有相依性 (包括具有模組資訊的 JAR) 都會放在類別路徑上。優點是您 (或其他) 模組的所有內部類別都可以在測試中直接存取。這可能是單元測試的完全有效設定,其中我們不關心較大的模組結構,而只關心測試單一函式。

如果您使用 Eclipse:預設情況下,Eclipse 也會使用模組修補程式執行單元測試 (請參閱下方)。在已匯入的 Gradle 專案中,使用 Eclipse 測試執行器測試模組可能會失敗。然後您需要手動調整測試執行設定中的類別路徑/模組路徑,或將測試執行委派給 Gradle。

這只涉及測試執行。單元測試編譯和開發在 Eclipse 中運作良好。

黑盒整合測試

對於整合測試,您可以選擇將測試設定本身定義為其他模組。您執行的步驟類似於將主要來源轉換為模組的方式:將 module-info.java 檔案新增到對應的原始碼設定 (例如 integrationTests/java/module-info.java)。

您可以在此找到包含黑盒整合測試的完整範例。

在 Eclipse 中,目前不支援在一個專案中編譯多個模組。因此,如果將測試移至獨立的子專案,這裡說明的整合測試 (黑盒) 設定才可以在 Eclipse 中運作。

使用模組修補程式的白盒測試執行

白盒測試的另一種方法是透過將測試修補到受測模組中,以留在模組世界中。這樣一來,模組界線會保留在原處,但測試本身會成為受測模組的一部分,然後可以存取模組的內部元件。

這在哪些使用案例中相關,以及如何以最佳方式執行,是討論的主題。目前沒有通用的最佳方法。因此,Gradle 目前不特別支援這一點。

不過,你可以這樣為測試設定模組修補

  • 將一個 module-info.java 加入你的測試來源組,它是主 module-info.java 的一份拷貝,並帶有測試所需的其他相依性(例如 requires org.junit.jupiter.api)。

  • 用參數設定 testCompileJavatest 任務,以使用測試類別修補主類別,如下所示。

build.gradle.kts
val moduleName = "org.gradle.sample"
val patchArgs = listOf("--patch-module", "$moduleName=${tasks.compileJava.get().destinationDirectory.asFile.get().path}")
tasks.compileTestJava {
    options.compilerArgs.addAll(patchArgs)
}
tasks.test {
    jvmArgs(patchArgs)
}
build.gradle
def moduleName = "org.gradle.sample"
def patchArgs = ["--patch-module", "$moduleName=${tasks.compileJava.destinationDirectory.asFile.get().path}"]
tasks.named('compileTestJava') {
    options.compilerArgs += patchArgs
}
tasks.named('test') {
    jvmArgs += patchArgs
}
如果使用自訂參數進行修補,Eclipse 和 IDEA 就不會選取這些參數。你很可能會在 IDE 中看到無效的編譯錯誤。

略過測試

如果你想在執行組建時略過測試,你可以選擇透過 命令列參數組建腳本 來執行。要在命令列中執行,你可以使用 -x--exclude-task 選項,如下所示

gradle build -x test

這會排除 test 任務和它專屬相依的任何其他任務,也就是說沒有其他任務相依於相同的任務。Gradle 不會將這些任務標記為「已略過」,但它們只會單純不會出現在執行的任務清單中。

透過組建腳本略過測試的方法有幾種。一種常見的方法是透過 Task.onlyIf(String, org.gradle.api.specs.Spec) 方法讓測試執行有條件。下列範例會在專案有一個名為 mySkipTests 的屬性的情況下略過 test 任務

build.gradle.kts
tasks.test {
    val skipTestsProvider = providers.gradleProperty("mySkipTests")
    onlyIf("mySkipTests property is not set") {
        !skipTestsProvider.isPresent()
    }
}
build.gradle
def skipTestsProvider = providers.gradleProperty('mySkipTests')
test.onlyIf("mySkipTests property is not set") {
    !skipTestsProvider.present
}

在這種情況下,Gradle 會將略過的測試標記為「已略過」,而不是將它們從組建中排除。

強制執行測試

在定義良好的組建中,你可以依賴 Gradle 僅在測試本身或生產程式碼變更時執行測試。不過,你可能會遇到測試依賴於第三方服務或其他可能會變更但無法在組建中建模的事項的情況。

你隨時可以使用 內建任務選項 --rerun 來強制任務重新執行。

gradle test --rerun

或者,如果 組建快取 沒有啟用,你也可以透過清除相關 Test 任務(例如 test)的輸出,並重新執行測試來強制執行測試,如下所示

gradle cleanTest test

cleanTest 是基於 基本外掛 提供的任務規則。你可以將它用於任何任務。

在執行測試時進行偵錯

在少數情況下,你會希望在測試執行時偵錯你的程式碼,這時如果你可以在那個時間點附加一個偵錯器,會很有幫助。你可以將 Test.getDebug() 屬性設定為 true 或使用 --debug-jvm 命令列選項,或使用 --no-debug-jvm 將它設定為 false。

當為測試啟用偵錯時,Gradle 會啟動測試程序,並在埠 5005 上暫停並監聽。

您也可以在 DSL 中啟用偵錯,您可以在其中設定其他屬性

test {
    debugOptions {
        enabled = true
        host = 'localhost'
        port = 4455
        server = true
        suspend = true
    }
}

使用此設定,測試 JVM 的行為就像傳遞 --debug-jvm 參數一樣,但它會在埠 4455 上監聽。

若要透過網路遠端偵錯測試程序,host 需要設定為電腦的 IP 位址或 "*"(在所有介面上監聽)。

使用測試固定裝置

在單一專案中製作和使用測試固定裝置

測試固定裝置通常用於設定受測程式碼,或提供旨在促進元件測試的公用程式。Java 專案可以透過套用 java-test-fixtures 外掛程式,以及 javajava-library 外掛程式,來啟用測試固定裝置支援

lib/build.gradle.kts
plugins {
    // A Java Library
    `java-library`
    // which produces test fixtures
    `java-test-fixtures`
    // and is published
    `maven-publish`
}
lib/build.gradle
plugins {
    // A Java Library
    id 'java-library'
    // which produces test fixtures
    id 'java-test-fixtures'
    // and is published
    id 'maven-publish'
}

這將自動建立一個 testFixtures 來源組,您可以在其中撰寫測試固定裝置。測試固定裝置的設定如下

  • 它們可以看到主要來源組類別

  • 測試來源可以看到測試固定裝置類別

例如,對於這個主要類別

src/main/java/com/acme/Person.java
public class Person {
    private final String firstName;
    private final String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    // ...

測試固定裝置可以在 src/testFixtures/java 中撰寫

src/testFixtures/java/com/acme/Simpsons.java
public class Simpsons {
    private static final Person HOMER = new Person("Homer", "Simpson");
    private static final Person MARGE = new Person("Marjorie", "Simpson");
    private static final Person BART = new Person("Bartholomew", "Simpson");
    private static final Person LISA = new Person("Elisabeth Marie", "Simpson");
    private static final Person MAGGIE = new Person("Margaret Eve", "Simpson");
    private static final List<Person> FAMILY = new ArrayList<Person>() {{
        add(HOMER);
        add(MARGE);
        add(BART);
        add(LISA);
        add(MAGGIE);
    }};

    public static Person homer() { return HOMER; }

    public static Person marge() { return MARGE; }

    public static Person bart() { return BART; }

    public static Person lisa() { return LISA; }

    public static Person maggie() { return MAGGIE; }

    // ...

宣告測試固定裝置的相依性

Java 函式庫外掛程式 類似,測試固定裝置會公開 API 和實作設定

lib/build.gradle.kts
dependencies {
    testImplementation("junit:junit:4.13")

    // API dependencies are visible to consumers when building
    testFixturesApi("org.apache.commons:commons-lang3:3.9")

    // Implementation dependencies are not leaked to consumers when building
    testFixturesImplementation("org.apache.commons:commons-text:1.6")
}
lib/build.gradle
dependencies {
    testImplementation 'junit:junit:4.13'

    // API dependencies are visible to consumers when building
    testFixturesApi 'org.apache.commons:commons-lang3:3.9'

    // Implementation dependencies are not leaked to consumers when building
    testFixturesImplementation 'org.apache.commons:commons-text:1.6'
}

值得注意的是,如果相依性是測試固定裝置的實作相依性,那麼在編譯依賴於這些測試固定裝置的測試時,實作相依性不會外洩到編譯類別路徑中。這會改善關注點分離,並提升編譯避免。

使用其他專案的測試固定裝置

測試固定裝置不限於單一專案。依賴專案的測試通常也需要依賴項的測試固定裝置。這可以使用 testFixtures 關鍵字輕鬆達成

build.gradle.kts
dependencies {
    implementation(project(":lib"))

    testImplementation("junit:junit:4.13")
    testImplementation(testFixtures(project(":lib")))
}
build.gradle
dependencies {
    implementation(project(":lib"))

    testImplementation 'junit:junit:4.13'
    testImplementation(testFixtures(project(":lib")))
}

發佈測試固定裝置

使用 java-test-fixtures 外掛的其中一個好處是測試固定裝置會被發佈。根據慣例,測試固定裝置會以具有 test-fixtures 分類器的成品發佈。對於 Maven 和 Ivy,具有該分類器的成品會與一般成品一起發佈。不過,如果您使用 maven-publishivy-publish 外掛,測試固定裝置會在 Gradle 模組中繼資料 中發佈為其他變體,而且您可以在其他 Gradle 專案中直接依賴外部函式庫的測試固定裝置

build.gradle.kts
dependencies {
    // Adds a dependency on the test fixtures of Gson, however this
    // project doesn't publish such a thing
    functionalTest(testFixtures("com.google.code.gson:gson:2.8.5"))
}
build.gradle
dependencies {
    // Adds a dependency on the test fixtures of Gson, however this
    // project doesn't publish such a thing
    functionalTest testFixtures("com.google.code.gson:gson:2.8.5")
}

值得注意的是,如果外部專案沒有發佈 Gradle 模組中繼資料,解析會失敗,並顯示找不到此類變體的錯誤

gradle dependencyInsight --configuration functionalTestClasspath --dependency gson 的輸出
> gradle dependencyInsight --configuration functionalTestClasspath --dependency gson

> Task :dependencyInsight
com.google.code.gson:gson:2.8.5 FAILED
   Failures:
      - Could not resolve com.google.code.gson:gson:2.8.5.
          - Unable to find a variant of com.google.code.gson:gson:2.8.5 providing the requested capability com.google.code.gson:gson-test-fixtures:
               - Variant compile provides com.google.code.gson:gson:2.8.5
               - Variant enforced-platform-compile provides com.google.code.gson:gson-derived-enforced-platform:2.8.5
               - Variant enforced-platform-runtime provides com.google.code.gson:gson-derived-enforced-platform:2.8.5
               - Variant javadoc provides com.google.code.gson:gson:2.8.5
               - Variant platform-compile provides com.google.code.gson:gson-derived-platform:2.8.5
               - Variant platform-runtime provides com.google.code.gson:gson-derived-platform:2.8.5
               - Variant runtime provides com.google.code.gson:gson:2.8.5
               - Variant sources provides com.google.code.gson:gson:2.8.5

com.google.code.gson:gson:2.8.5 FAILED
\--- functionalTestClasspath

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

錯誤訊息提到缺少 com.google.code.gson:gson-test-fixtures 功能,這的確未定義於此函式庫中。這是因為根據慣例,對於使用 java-test-fixtures 外掛的專案,Gradle 會自動建立測試固定裝置變體,其功能名稱為主要元件名稱,加上附錄 -test-fixtures

如果您發佈函式庫並使用測試固定裝置,但不想發佈固定裝置,您可以停用測試固定裝置變體的發佈,如下所示。
build.gradle.kts
val javaComponent = components["java"] as AdhocComponentWithVariants
javaComponent.withVariantsFromConfiguration(configurations["testFixturesApiElements"]) { skip() }
javaComponent.withVariantsFromConfiguration(configurations["testFixturesRuntimeElements"]) { skip() }
build.gradle
components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() }
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }

1. JUnit wiki 詳細說明如何使用 JUnit 類別:https://github.com/junit-team/junit/wiki/Categories
2。TestNG 文件包含更多關於測試群組的詳細資訊:http://testng.org/doc/documentation-main.html#test-groups
3。TestNG 文件包含更多關於使用 testng.xml 檔案時測試順序的詳細資訊:http://testng.org/doc/documentation-main.html#testng-xml