在 JVM 上進行測試是一個豐富的主題。有許多不同的測試函式庫和框架,以及許多不同類型的測試。所有這些都需要成為建置的一部分,無論它們是否頻繁執行。本章專門解釋 Gradle 如何處理建置之間和建置內部的不同需求,並重點介紹它如何與兩個最常見的測試框架整合:JUnitTestNG

它解釋了

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

透過孵化中的 JVM 測試套件外掛,可以使用新的配置 DSL 來建模測試執行階段。

基本概念

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

為了運作,Test 任務類型只需要兩項資訊

當您使用 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 停用它。

有關所有可用配置選項的詳細資訊,請參閱 Test

如果配置不正確,測試過程可能會意外退出。例如,如果 Java 可執行檔不存在或提供了無效的 JVM 引數,則測試過程將無法啟動。同樣地,如果測試對測試過程進行程式化變更,也可能導致意外失敗。

例如,如果在測試中修改了 SecurityManager,則可能會發生問題,因為 Gradle 的內部訊息傳遞依賴於反射和套接字通訊,如果安全性管理員的權限變更,則可能會中斷這些通訊。在這種特殊情況下,您應該在測試後還原原始 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 測試報告

  • 與 Ant JUnit 報告任務相容的 XML 測試結果格式 — 許多其他工具 (例如 CI 伺服器) 支援的格式

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

在大多數情況下,您將使用標準 HTML 報告,該報告會自動包含所有 Test 任務的結果,即使是您自己明確新增到建置中的任務也是如此。例如,如果您為整合測試新增 Test 任務,則報告將包含單元測試和整合測試的結果 (如果兩個任務都已執行)。

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

與許多測試配置選項不同,有幾個專案層級的 慣例屬性會影響測試報告。例如,您可以像這樣變更測試結果和報告的目的地

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

tasks.register("showDirs") {
    val settingsDir = project.layout.settingsDirectory.asFile
    val reportsDir = project.reporting.baseDirectory
    val testResultsDir = project.java.testResultsDir

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

tasks.register('showDirs') {
    def settingsDir = project.layout.settingsDirectory.asFile
    def reportsDir = project.reporting.baseDirectory
    def testResultsDir = project.java.testResultsDir

    doLast {
        logger.quiet(settingsDir.toPath().relativize(reportsDir.get().asFile.toPath()).toString())
        logger.quiet(settingsDir.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 伺服器和其他工具

Test 任務會以「JUnit XML」偽標準建立描述測試結果的 XML 檔案。JUnit 4、JUnit Jupiter 和 TestNG 測試框架都使用此標準,並且針對每個框架使用相同的 DSL 區塊進行配置。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 {
            includeSystemOutLog = false // defaults to true
            includeSystemErrLog = false // defaults to true
            isOutputPerTestCase = true // defaults to false
            mergeReruns = true // defaults to false
        }
    }
}
build.gradle
test {
    reports {
        junitXml {
            includeSystemOutLog = false // defaults to true
            includeSystemErrLog = false // defaults to true
            outputPerTestCase = true // defaults to false
            mergeReruns = true // defaults to false
        }
    }
}
includeSystemOutLog 和 includeSystemErrLog

includeSystemOutLog 選項允許配置是否將寫入標準輸出的測試輸出匯出到 XML 報告檔案。includeSystemErrLog 選項允許配置是否將寫入標準錯誤的測試錯誤輸出匯出到 XML 報告檔案。

這些選項會影響測試套件層級輸出 (例如 @BeforeClass/@BeforeAll 輸出) 以及測試類別和方法特定輸出 (@Before/@BeforeEach@Test)。如果停用任一選項,通常包含該內容的元素將從 XML 報告檔案中排除。

每個選項的預設值為 true

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 註釋的方法,或超類別包含使用 @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 文件 更詳細地解釋了差異,但基本上,如果您有一個依賴於 B() 的測試方法 A(),則按實例分組可確保在執行下一個配對之前,執行每個 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 標記為「SKIPPED」,而只會簡單地不會出現在已執行任務的清單中。

透過建置腳本跳過測試可以透過幾種方式完成。一種常見的方法是透過 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 會將跳過的測試標記為「SKIPPED」,而不是從建置中排除它們。

強制執行測試

在定義完善的建置中,您可以依靠 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 來源集,您可以在其中編寫測試夾具。測試夾具配置為

  • 它們可以看到main 來源集類別

  • test 來源可以看到測試夾具類別

例如,對於此主要類別

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 Library 外掛程式 類似,測試夾具公開了 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 with the requested capability: feature '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 文件包含有關測試群組的更多詳細資訊:https://testng.org/#_test_groups
3. TestNG 文件包含有關使用 testng.xml 檔案時測試排序的更多詳細資訊:http://testng.org/doc/documentation-main.html#testng-xml