Java 與 JVM 專案的測試
基本概念
所有 JVM 測試都圍繞著單一任務類型:Test。這會使用任何支援的測試函式庫 — JUnit、JUnit Platform 或 TestNG — 執行一系列測試案例,並整理結果。然後,您可以透過 TestReport 任務類型的實例將這些結果轉換為報告。
為了運作,Test
任務類型只需要兩項資訊
-
在哪裡找到已編譯的測試類別 (屬性:Test.getTestClassesDirs())
-
執行類別路徑,其中應包含受測類別以及您正在使用的測試函式庫 (屬性:Test.getClasspath())
當您使用 JVM 語言外掛 — 例如 Java 外掛 — 時,您將自動獲得以下內容
-
用於單元測試的專用
test
來源集 -
執行這些單元測試的
Test
類型的test
任務
JVM 語言外掛使用來源集來配置任務,使其具有適當的執行類別路徑和包含已編譯測試類別的目錄。此外,它們還將 test
任務附加到 check
生命週期任務。
還值得注意的是,test
來源集會自動建立 對應的相依性配置 — 其中最有用的配置是 testImplementation
和 testRuntimeOnly
— 外掛會將其繫結到 test
任務的類別路徑。
在大多數情況下,您需要做的就是配置適當的編譯和執行階段相依性,並向 test
任務新增任何必要的配置。以下範例顯示了一個簡單的設定,它使用 JUnit Platform 並將測試 JVM 的最大堆積大小變更為 1 GB
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")
}
}
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 任務有許多通用配置選項,以及一些框架特定的選項,您可以在 JUnitOptions、JUnitPlatformOptions 和 TestNGOptions 中找到描述。我們在本章的其餘部分涵蓋了其中相當多的選項。
如果您想使用自己的一組測試類別來設定自己的 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.SomeTest
、org.gradle.SomeTest.someMethod
-
如果模式以大寫字母開頭,則為簡單類別名稱或方法名稱,例如
SomeTest
、SomeTest.someMethod
(自 Gradle 4.7 起) -
'*' 萬用字元比對
您可以在建置腳本中或透過 --tests
命令列選項啟用篩選。以下是一些每次建置執行時都會套用的篩選範例
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")
}
}
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
任務,則報告將包含單元測試和整合測試的結果 (如果兩個任務都已執行)。
若要彙總多個子專案的測試結果,請參閱 測試報告彙總外掛。 |
與許多測試配置選項不同,有幾個專案層級的 慣例屬性會影響測試報告。例如,您可以像這樣變更測試結果和報告的目的地
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())
}
}
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
的值和您想要包含在報告中的測試結果。以下是一個範例,它會產生所有子專案的單元測試的合併報告
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() })
}
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)
}
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)
}
}
// 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")
,每個測試類別一個檔案。可以針對專案的所有測試任務或個別針對每個測試任務變更位置。
java.testResultsDir = layout.buildDirectory.dir("junit-xml")
java.testResultsDir = layout.buildDirectory.dir("junit-xml")
使用上述配置,XML 檔案將寫入 layout.buildDirectory.dir("junit-xml/$testTaskName")
。
tasks.test {
reports {
junitXml.outputLocation = layout.buildDirectory.dir("test-junit-xml")
}
}
test {
reports {
junitXml.outputLocation = layout.buildDirectory.dir("test-junit-xml")
}
}
使用上述配置,test
任務的 XML 檔案將寫入 layout.buildDirectory.dir("test-results/test-junit-xml")
。其他測試任務的 XML 檔案位置將保持不變。
配置選項
也可以配置 XML 檔案的內容,以透過配置 JUnitXmlReport 選項以不同的方式傳達結果。
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
}
}
}
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 測試
-
最終繼承自
TestCase
或GroovyTestCase
-
使用
@RunWith
註釋 -
包含使用
@Test
註釋的方法,或超類別包含使用@Test
註釋的方法
對於 TestNG,Gradle 會掃描使用 @Test
註釋的方法。
請注意,抽象類別不會執行。此外,請注意,Gradle 會向上掃描繼承樹到測試類別路徑上的 jar 檔案中。因此,如果這些 JAR 包含測試類別,它們也將執行。
如果您不想使用測試類別偵測,您可以透過將 Test 上的 scanForTestClasses
屬性設定為 false
來停用它。當您這樣做時,測試任務僅使用 includes
和 excludes
屬性來尋找測試類別。
如果 scanForTestClasses
為 false 且未指定任何包含或排除模式,則 Gradle 預設為執行符合模式 **/*Tests.class
和 **/*Test.class
的任何類別,但排除符合 **/Abstract*.class
的類別。
使用 JUnit Platform 時,僅使用 includes 和 excludes 來篩選測試類別 — scanForTestClasses 不會生效。 |
測試日誌記錄
Gradle 允許對記錄到主控台的事件進行微調控制。日誌記錄可針對每個日誌級別進行配置,預設情況下,會記錄以下事件
當日誌級別為 |
記錄的事件 |
其他配置 |
|
無 |
無 |
|
例外格式為 SHORT |
|
|
堆疊追蹤會被截斷。 |
|
|
會記錄完整的堆疊追蹤。 |
可以針對每個日誌級別修改測試日誌記錄,方法是調整測試任務的 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
中的測試
tasks.test {
useJUnit {
includeCategories("org.gradle.junit.CategoryA")
excludeCategories("org.gradle.junit.CategoryB")
}
}
test {
useJUnit {
includeCategories 'org.gradle.junit.CategoryA'
excludeCategories 'org.gradle.junit.CategoryB'
}
}
JUnit Platform 引入了 標籤 來取代類別。您可以透過 Test.useJUnitPlatform(org.gradle.api.Action) 指定包含/排除的標籤,如下所示
TestNG 框架使用測試群組的概念來達到類似的效果。[2] 您可以透過 Test.useTestNG(org.gradle.api.Action) 設定,配置在測試執行期間要包含或排除哪些測試群組,如下所示
tasks.named<Test>("test") {
useTestNG {
val options = this as TestNGOptions
options.excludeGroups("integrationTests")
options.includeGroups("unitTests")
}
}
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 支援
tasks.named<Test>("test") {
useJUnitPlatform()
}
tasks.named('test', Test) {
useJUnitPlatform()
}
請參閱 Test.useJUnitPlatform() 以取得更多詳細資訊。
編譯和執行 JUnit Jupiter 測試
若要在 Gradle 中啟用 JUnit Jupiter 支援,您只需新增以下依賴項
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
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 依賴項
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")
}
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-engine 和 junit-vintage-engine。您也可以編寫並插入您自己的 TestEngine
實作,如 此處 所述。
預設情況下,將使用測試執行時期類別路徑上的所有測試引擎。若要明確控制特定的測試引擎實作,您可以將以下設定新增至您的建置腳本
tasks.withType<Test>().configureEach {
useJUnitPlatform {
includeEngines("junit-vintage")
// excludeEngines("junit-jupiter")
}
}
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
將會導致建置失敗。
tasks.test {
useTestNG {
preserveOrder = true
}
}
test {
useTestNG {
preserveOrder = true
}
}
groupByInstance
屬性控制是否應按實例而不是按類別對測試進行分組。TestNG 文件 更詳細地解釋了差異,但基本上,如果您有一個依賴於 B()
的測試方法 A()
,則按實例分組可確保在執行下一個配對之前,執行每個 A-B 配對,例如 B(1)
-A(1)
。使用按類別分組,將先執行所有 B()
方法,然後執行所有 A()
方法。
請注意,如果您使用資料提供者來參數化測試,則通常只會有一個以上的測試實例。此外,按實例對測試進行分組是在 TestNG 6.1 版本中引入的。針對較舊的 TestNG 版本將 groupByInstances
屬性設定為 true
將會導致建置失敗。
tasks.test {
useTestNG {
groupByInstances = true
}
}
test {
useTestNG {
groupByInstances = true
}
}
TestNG 參數化方法和報告
TestNG 支援 參數化測試方法,允許使用不同的輸入多次執行特定的測試方法。Gradle 會在其測試方法執行的報告中包含參數值。
給定一個名為 aTestMethod
的參數化測試方法,它接受兩個參數,它將以名稱 aTestMethod(toStringValueOfParam1, toStringValueOfParam2)
報告。這使得很容易識別特定迭代的參數值。
配置整合測試
專案的常見需求是以某種形式納入整合測試。它們的目的是驗證專案的各個部分是否能正常協同工作。這通常表示與單元測試相比,它們需要特殊的執行設定和依賴項。
將整合測試新增到您的建置的最簡單方法是利用仍在開發中的 JVM 測試套件 外掛程式。如果仍在開發中的解決方案不適合您,以下是您需要在建置中採取的步驟
-
為它們建立新的 來源集
-
將您需要的依賴項新增到該來源集的適當配置中
-
配置該來源集的編譯和執行時期類別路徑
-
建立執行整合測試的任務
您可能還需要執行一些額外的配置,具體取決於整合測試的形式。我們將在討論過程中討論這些內容。
讓我們從一個實用範例開始,該範例在建置腳本中實作了前三個步驟,以新的來源集 intTest
為中心
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")
}
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
的新來源集,它會自動建立
-
intTestImplementation
、intTestCompileOnly
、intTestRuntimeOnly
配置(以及 其他一些 較少需要的配置) -
一個
compileIntTestJava
任務,它將編譯 src/intTest/java 下的所有來源檔案
如果您正在使用 IntelliJ IDE,您可能希望將這些額外來源集中的目錄標記為包含測試來源而不是生產來源,如 Idea 外掛程式 文件中所述。 |
此範例也執行以下操作,並非所有操作都是您的特定整合測試可能需要的
-
將
main
來源集的生產類別新增到整合測試的編譯和執行時期類別路徑 —sourceSets.main.output
是包含已編譯生產類別和資源的所有目錄的 檔案集合 -
使
intTestImplementation
配置從implementation
擴充,這表示生產程式碼的所有宣告依賴項也成為整合測試的依賴項 -
對
intTestRuntimeOnly
配置執行相同的操作
在大多數情況下,您希望您的整合測試能夠存取受測類別,這就是為什麼我們確保這些類別包含在此範例中的編譯和執行時期類別路徑中。但是,某些類型的測試會以不同的方式與生產程式碼互動。例如,您可能有將您的應用程式作為可執行檔執行的測試,並驗證輸出。對於 Web 應用程式,測試可能會透過 HTTP 與您的應用程式互動。由於在這種情況下,測試不需要直接存取受測類別,因此您不需要將生產類別新增到測試類別路徑。
另一個常見步驟是將所有單元測試依賴項也附加到整合測試 — 透過 intTestImplementation.extendsFrom testImplementation
— 但這僅在整合測試需要所有或幾乎所有單元測試具有的相同依賴項時才有意義。
您應該注意此範例的其他幾個方面
-
+=
允許您將路徑和路徑集合附加到compileClasspath
和runtimeClasspath
,而不是覆寫它們 -
如果您想使用基於慣例的配置,例如
intTestImplementation
,您必須在新來源集之後宣告依賴項
建立和配置來源集會自動設定編譯階段,但它在執行整合測試方面沒有任何作用。因此,最後一塊拼圖是一個自訂測試任務,它使用來自新來源集的資訊來配置其執行時期類別路徑和測試類別
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) }
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
)。 -
使用參數配置
testCompileJava
和test
任務,以使用測試類別修補主要類別,如下所示。
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)
}
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 中看到無效的編譯錯誤。 |
跳過測試
gradle build -x test
這會排除 test
任務和任何其他專門依賴它的任務,即沒有其他任務依賴相同的任務。這些任務不會被 Gradle 標記為「SKIPPED」,而只會簡單地不會出現在已執行任務的清單中。
透過建置腳本跳過測試可以透過幾種方式完成。一種常見的方法是透過 Task.onlyIf(String, org.gradle.api.specs.Spec) 方法使測試執行成為有條件的。如果專案具有名為 mySkipTests
的屬性,則以下範例會跳過 test
任務
tasks.test {
val skipTestsProvider = providers.gradleProperty("mySkipTests")
onlyIf("mySkipTests property is not set") {
!skipTestsProvider.isPresent()
}
}
def skipTestsProvider = providers.gradleProperty('mySkipTests')
test.onlyIf("mySkipTests property is not set") {
!skipTestsProvider.present
}
在這種情況下,Gradle 會將跳過的測試標記為「SKIPPED」,而不是從建置中排除它們。
在執行測試時進行偵錯
在少數您想要在測試執行時偵錯程式碼的情況下,如果您可以在該點附加偵錯工具,可能會很有幫助。您可以將 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
外掛程式以及 java
或 java-library
外掛程式來啟用測試夾具支援
plugins {
// A Java Library
`java-library`
// which produces test fixtures
`java-test-fixtures`
// and is published
`maven-publish`
}
plugins {
// A Java Library
id 'java-library'
// which produces test fixtures
id 'java-test-fixtures'
// and is published
id 'maven-publish'
}
這將自動建立一個 testFixtures
來源集,您可以在其中編寫測試夾具。測試夾具配置為
-
它們可以看到main 來源集類別
-
test 來源可以看到測試夾具類別
例如,對於此主要類別
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
中編寫測試夾具
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 和實作配置
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")
}
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
關鍵字非常輕鬆地實現
dependencies {
implementation(project(":lib"))
testImplementation("junit:junit:4.13")
testImplementation(testFixtures(project(":lib")))
}
dependencies {
implementation(project(":lib"))
testImplementation 'junit:junit:4.13'
testImplementation(testFixtures(project(":lib")))
}
發佈測試夾具
使用 java-test-fixtures
外掛程式的優點之一是測試夾具已發佈。依照慣例,測試夾具將與具有 test-fixtures
分類器的成品一起發佈。對於 Maven 和 Ivy,具有該分類器的成品只是與常規成品一起發佈。但是,如果您使用 maven-publish
或 ivy-publish
外掛程式,則測試夾具會作為 Gradle 模組中繼資料 中的其他變體發佈,並且您可以直接依賴另一個 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"))
}
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
。
如果您發佈程式庫並使用測試夾具,但不想要發佈夾具,您可以停用測試夾具變體的發佈,如下所示。 |
val javaComponent = components["java"] as AdhocComponentWithVariants
javaComponent.withVariantsFromConfiguration(configurations["testFixturesApiElements"]) { skip() }
javaComponent.withVariantsFromConfiguration(configurations["testFixturesRuntimeElements"]) { skip() }
components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() }
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }
testng.xml
檔案時測試排序的更多詳細資訊:http://testng.org/doc/documentation-main.html#testng-xml。