使用 TestKit 測試建置邏輯
Gradle TestKit(又名 TestKit)是一個程式庫,旨在協助測試 Gradle 外掛和一般建置邏輯。目前,它專注於功能測試。也就是說,透過將建置邏輯作為程式化執行的建置一部分來進行測試。隨著時間的推移,TestKit 很有可能會擴展以促進其他類型的測試。
使用 TestKit
若要使用 TestKit,請在您外掛的建置中包含以下內容
dependencies {
testImplementation(gradleTestKit())
}
dependencies {
testImplementation gradleTestKit()
}
gradleTestKit()
包含 TestKit 的類別,以及 Gradle Tooling API 用戶端。它不包含 JUnit、TestNG 或任何其他測試執行框架的版本。此類相依性必須明確宣告。
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.named('test', Test) {
useJUnitPlatform()
}
使用 Gradle 執行器進行功能測試
GradleRunner 方便以程式化方式執行 Gradle 建置,並檢查結果。
可以建立一個人為建構的建置(例如,以程式化方式或從範本),以測試「受測邏輯」。然後可以執行建置,可能以各種方式執行(例如,任務和引數的不同組合)。然後可以透過斷言以下內容來驗證邏輯的正確性,可能以組合方式驗證
-
建置的輸出;
-
建置的記錄(即主控台輸出);
-
建置執行的任務集及其結果(例如,FAILED、UP-TO-DATE 等)。
在建立和配置執行器實例後,可以透過 GradleRunner.build() 或 GradleRunner.buildAndFail() 方法執行建置,具體取決於預期的結果。
以下示範了在 Java JUnit 測試中使用 Gradle 執行器
範例:將 GradleRunner 與 Java 和 JUnit 搭配使用
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class BuildLogicFunctionalTest {
@TempDir File testProjectDir;
private File settingsFile;
private File buildFile;
@BeforeEach
public void setup() {
settingsFile = new File(testProjectDir, "settings.gradle");
buildFile = new File(testProjectDir, "build.gradle");
}
@Test
public void testHelloWorldTask() throws IOException {
writeFile(settingsFile, "rootProject.name = 'hello-world'");
String buildFileContent = "task helloWorld {" +
" doLast {" +
" println 'Hello world!'" +
" }" +
"}";
writeFile(buildFile, buildFileContent);
BuildResult result = GradleRunner.create()
.withProjectDir(testProjectDir)
.withArguments("helloWorld")
.build();
assertTrue(result.getOutput().contains("Hello world!"));
assertEquals(SUCCESS, result.task(":helloWorld").getOutcome());
}
private void writeFile(File destination, String content) throws IOException {
BufferedWriter output = null;
try {
output = new BufferedWriter(new FileWriter(destination));
output.write(content);
} finally {
if (output != null) {
output.close();
}
}
}
}
可以使用任何測試執行框架。
由於 Gradle 建置腳本也可以使用 Groovy 程式語言編寫,因此通常在 Groovy 中編寫 Gradle 功能測試是一個有效率的選擇。此外,建議使用(基於 Groovy 的)Spock 測試執行框架,因為它比使用 JUnit 提供了許多引人注目的功能。
以下示範了在 Groovy Spock 測試中使用 Gradle 執行器
範例:將 GradleRunner 與 Groovy 和 Spock 搭配使用
import org.gradle.testkit.runner.GradleRunner
import static org.gradle.testkit.runner.TaskOutcome.*
import spock.lang.TempDir
import spock.lang.Specification
class BuildLogicFunctionalTest extends Specification {
@TempDir File testProjectDir
File settingsFile
File buildFile
def setup() {
settingsFile = new File(testProjectDir, 'settings.gradle')
buildFile = new File(testProjectDir, 'build.gradle')
}
def "hello world task prints hello world"() {
given:
settingsFile << "rootProject.name = 'hello-world'"
buildFile << """
task helloWorld {
doLast {
println 'Hello world!'
}
}
"""
when:
def result = GradleRunner.create()
.withProjectDir(testProjectDir)
.withArguments('helloWorld')
.build()
then:
result.output.contains('Hello world!')
result.task(":helloWorld").outcome == SUCCESS
}
}
常見的做法是以獨立專案中的外部類別實作任何本質上更複雜的自訂建置邏輯(如外掛和任務類型)。此方法背後的主要驅動力是將編譯後的程式碼捆綁到 JAR 檔案中,發佈到二進制儲存庫並在各種專案中重複使用。
將受測外掛放入測試建置中
GradleRunner 使用 Tooling API 執行建置。其含義是建置在單獨的進程中執行(即,與執行測試的進程不同)。因此,測試建置與測試進程不共享相同的類別路徑或類別載入器,並且受測程式碼不會隱式地提供給測試建置。
GradleRunner 支援與 Tooling API 相同的 Gradle 版本範圍。支援的版本在相容性矩陣中定義。 使用較舊 Gradle 版本的建置可能仍然有效,但不保證。 |
從版本 2.13 開始,Gradle 提供了一種傳統機制,將受測程式碼注入到測試建置中。
使用 Java Gradle 外掛開發外掛自動注入
Java Gradle 外掛開發外掛可用於協助開發 Gradle 外掛。從 Gradle 版本 2.13 開始,此外掛提供與 TestKit 的直接整合。當應用於專案時,此外掛會自動將 gradleTestKit()
相依性新增至 testApi
配置。此外,它會自動產生受測程式碼的類別路徑,並透過 GradleRunner.withPluginClasspath() 為使用者建立的任何 GradleRunner
實例注入該路徑。重要的是要注意,目前只有在使用 plugins DSL 應用受測外掛時,此機制才有效。如果 目標 Gradle 版本 早於 2.8,則不會執行自動外掛類別路徑注入。
此外掛使用以下慣例來應用 TestKit 相依性並注入類別路徑
-
包含受測程式碼的來源集:
sourceSets.main
-
用於注入外掛類別路徑的來源集:
sourceSets.test
可以使用類別 GradlePluginDevelopmentExtension 重新配置這些慣例中的任何一個。
以下基於 Groovy 的範例示範如何透過使用 Java Gradle 外掛開發外掛應用的標準慣例來自動注入外掛類別路徑。
plugins {
groovy
`java-gradle-plugin`
}
dependencies {
testImplementation("org.spockframework:spock-core:2.2-groovy-3.0") {
exclude(group = "org.codehaus.groovy")
}
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
plugins {
id 'groovy'
id 'java-gradle-plugin'
}
dependencies {
testImplementation('org.spockframework:spock-core:2.2-groovy-3.0') {
exclude group: 'org.codehaus.groovy'
}
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
範例:自動將受測程式碼類別注入到測試建置中
def "hello world task prints hello world"() {
given:
settingsFile << "rootProject.name = 'hello-world'"
buildFile << """
plugins {
id 'org.gradle.sample.helloworld'
}
"""
when:
def result = GradleRunner.create()
.withProjectDir(testProjectDir)
.withArguments('helloWorld')
.withPluginClasspath()
.build()
then:
result.output.contains('Hello world!')
result.task(":helloWorld").outcome == SUCCESS
}
以下建置腳本示範如何為使用自訂 Test
來源集的專案重新配置 Java Gradle 外掛開發外掛提供的慣例。
透過孵化中的 JVM 測試套件外掛,可以使用新的配置 DSL 為以下 functionalTest 套件建模。 |
plugins {
groovy
`java-gradle-plugin`
}
val functionalTest = sourceSets.create("functionalTest")
val functionalTestTask = tasks.register<Test>("functionalTest") {
group = "verification"
testClassesDirs = functionalTest.output.classesDirs
classpath = functionalTest.runtimeClasspath
useJUnitPlatform()
}
tasks.check {
dependsOn(functionalTestTask)
}
gradlePlugin {
testSourceSets(functionalTest)
}
dependencies {
"functionalTestImplementation"("org.spockframework:spock-core:2.2-groovy-3.0") {
exclude(group = "org.codehaus.groovy")
}
"functionalTestRuntimeOnly"("org.junit.platform:junit-platform-launcher")
}
plugins {
id 'groovy'
id 'java-gradle-plugin'
}
def functionalTest = sourceSets.create('functionalTest')
def functionalTestTask = tasks.register('functionalTest', Test) {
group = 'verification'
testClassesDirs = sourceSets.functionalTest.output.classesDirs
classpath = sourceSets.functionalTest.runtimeClasspath
useJUnitPlatform()
}
tasks.named("check") {
dependsOn functionalTestTask
}
gradlePlugin {
testSourceSets sourceSets.functionalTest
}
dependencies {
functionalTestImplementation('org.spockframework:spock-core:2.2-groovy-3.0') {
exclude group: 'org.codehaus.groovy'
}
functionalTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
控制建置環境
執行器透過在 JVM 暫存目錄(即 java.io.tmpdir
系統屬性指定的位置,通常為 /tmp
)內的目錄中指定專用的「工作目錄」,在隔離的環境中執行測試建置。預設 Gradle 使用者首頁(例如 ~/.gradle/gradle.properties
)中的任何配置都不會用於測試執行。TestKit 沒有公開用於精細控制環境所有方面的機制(例如,JDK)。未來版本的 TestKit 將提供改進的配置選項。
TestKit 使用專用的守護進程,這些進程會在測試執行後自動關閉。
專用的工作目錄不會在建置後由執行器刪除。TestKit 提供了兩種方法來指定定期清理的位置,例如專案的建置資料夾
-
org.gradle.testkit.dir
系統屬性;
設定用於測試的 Gradle 版本
Gradle 執行器需要 Gradle 發行版才能執行建置。TestKit 並不依賴 Gradle 的所有實作。
預設情況下,執行器將嘗試根據載入 GradleRunner
類別的位置尋找 Gradle 發行版。也就是說,預期該類別是從 Gradle 發行版載入的,就像使用 gradleTestKit()
相依性宣告時一樣。
當將執行器用作由 Gradle 執行的測試的一部分時(例如,執行外掛專案的 test
任務),執行測試的相同發行版將由執行器使用。當將執行器用作由 IDE 執行的測試的一部分時,匯入專案時使用的相同 Gradle 發行版將被使用。這表示外掛實際上將使用與建置外掛相同的 Gradle 版本進行測試。
或者,可以使用以下任何 GradleRunner
方法指定要使用的不同且特定的 Gradle 版本
這可以用於跨 Gradle 版本測試建置邏輯。以下示範了作為 Groovy Spock 測試編寫的跨版本相容性測試
範例:指定用於測試執行的 Gradle 版本
import org.gradle.testkit.runner.GradleRunner
import static org.gradle.testkit.runner.TaskOutcome.*
import spock.lang.TempDir
import spock.lang.Specification
class BuildLogicFunctionalTest extends Specification {
@TempDir File testProjectDir
File settingsFile
File buildFile
def setup() {
settingsFile = new File(testProjectDir, 'settings.gradle')
buildFile = new File(testProjectDir, 'build.gradle')
}
def "can execute hello world task with Gradle version #gradleVersion"() {
given:
buildFile << """
task helloWorld {
doLast {
logger.quiet 'Hello world!'
}
}
"""
settingsFile << ""
when:
def result = GradleRunner.create()
.withGradleVersion(gradleVersion)
.withProjectDir(testProjectDir)
.withArguments('helloWorld')
.build()
then:
result.output.contains('Hello world!')
result.task(":helloWorld").outcome == SUCCESS
where:
gradleVersion << ['5.0', '6.0.1']
}
}
使用不同 Gradle 版本進行測試時的功能支援
可以使用 GradleRunner 執行使用 Gradle 1.0 及更高版本的建置。但是,某些執行器功能在早期版本中不受支援。在這種情況下,當嘗試使用該功能時,執行器將擲回例外狀況。
下表列出了對使用的 Gradle 版本敏感的功能。
功能 | 最低版本 | 描述 |
---|---|---|
檢查已執行的任務 |
2.5 |
檢查已執行的任務,使用 BuildResult.getTasks() 和類似方法。 |
2.8 |
透過 GradleRunner.withPluginClasspath(java.lang.Iterable) 注入受測程式碼。 |
|
2.9 |
在偵錯模式下執行時,使用 BuildResult.getOutput() 檢查建置的文字輸出。 |
|
2.13 |
透過應用 Java Gradle 外掛開發外掛,透過 GradleRunner.withPluginClasspath() 自動注入受測程式碼。 |
|
設定建置要使用的環境變數。 |
3.5 |
Gradle Tooling API 僅在較新版本中支援設定環境變數。 |
偵錯建置邏輯
執行器使用 Tooling API 執行建置。其含義是建置在單獨的進程中執行(即,與執行測試的進程不同)。因此,在偵錯模式下執行測試不允許您像預期的那樣偵錯建置邏輯。IDE 中設定的任何中斷點都不會被測試建置執行的程式碼觸發。
TestKit 提供了兩種不同的方式來啟用偵錯模式
-
將「org.gradle.testkit.debug」系統屬性設定為
true
,用於使用GradleRunner
的 JVM(即,不是使用執行器執行的建置);
當希望在不對執行器配置進行臨時變更的情況下啟用偵錯支援時,可以使用系統屬性方法。大多數 IDE 都提供為測試執行設定 JVM 系統屬性的功能,並且可以使用此功能來設定此系統屬性。
使用建置快取進行測試
若要在測試中啟用 建置快取,您可以將 --build-cache
引數傳遞給 GradleRunner,或使用 啟用建置快取中描述的其他方法之一。然後,當您外掛的自訂任務被快取時,您可以檢查任務結果 TaskOutcome.FROM_CACHE。此結果僅對 Gradle 3.5 及更新版本有效。
範例:測試可快取的任務
def "cacheableTask is loaded from cache"() {
given:
buildFile << """
plugins {
id 'org.gradle.sample.helloworld'
}
"""
when:
def result = runner()
.withArguments( '--build-cache', 'cacheableTask')
.build()
then:
result.task(":cacheableTask").outcome == SUCCESS
when:
new File(testProjectDir, 'build').deleteDir()
result = runner()
.withArguments( '--build-cache', 'cacheableTask')
.build()
then:
result.task(":cacheableTask").outcome == FROM_CACHE
}
請注意,TestKit 會在測試之間重複使用 Gradle 使用者首頁(請參閱 GradleRunner.withTestKitDir(java.io.File)),其中包含本機建置快取的預設位置。為了使用建置快取進行測試,應在測試之間清理建置快取目錄。完成此操作的最簡單方法是將本機建置快取配置為使用暫存目錄。
範例:在測試之間清理建置快取
@TempDir File testProjectDir
File buildFile
File localBuildCacheDirectory
def setup() {
localBuildCacheDirectory = new File(testProjectDir, 'local-cache')
buildFile = new File(testProjectDir,'settings.gradle') << """
buildCache {
local {
directory = '${localBuildCacheDirectory.toURI()}'
}
}
"""
buildFile = new File(testProjectDir,'build.gradle')
}