整合測試應用程式模組
Spring Modulith 允許在隔離或與其他模組組合的情況下引導單個應用程式模組以執行整合測試。為此,請將 Spring Modulith 測試 starter 新增到您的專案中,如下所示
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
並將 JUnit 測試類放置在應用程式模組包或其任何子包中,並使用 @ApplicationModuleTest 註解它。
-
Java
-
Kotlin
package example.order;
@ApplicationModuleTest
class OrderIntegrationTests {
// Individual test cases go here
}
package example.order
@ApplicationModuleTest
class OrderIntegrationTests {
// Individual test cases go here
}
這將執行您的整合測試,類似於 @SpringBootTest 所實現的效果,但載入程式實際上僅限於測試所在的應用程式模組。如果您將 org.springframework.modulith 的日誌級別配置為 DEBUG,您將看到有關測試執行如何自定義 Spring Boot 引導的詳細資訊。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.0-SNAPSHOT)
… - Bootstrapping @ApplicationModuleTest for example.order in mode STANDALONE (class example.Application)…
… - ======================================================================================================
… - ## example.order ##
… - > Logical name: order
… - > Base package: example.order
… - > Direct module dependencies: none
… - > Spring beans:
… - + ….OrderManagement
… - + ….internal.OrderInternal
… - Starting OrderIntegrationTests using Java 17.0.3 …
… - No active profile set, falling back to 1 default profile: "default"
… - Re-configuring auto-configuration and entity scan packages to: example.order.
請注意,輸出中包含了測試執行中包含的模組的詳細資訊。它建立應用程式模組,找到要執行的模組,並將自動配置、元件和實體掃描的應用程式限制到相應的包。
引導模式
應用程式模組測試可以透過多種模式進行引導
-
STANDALONE(預設) — 僅運行當前模組。 -
DIRECT_DEPENDENCIES— 運行當前模組以及當前模組直接依賴的所有模組。 -
ALL_DEPENDENCIES— 運行當前模組以及整個依賴模組樹。
處理傳出依賴
當應用程式模組被引導時,它包含的 Spring bean 將被例項化。如果這些 bean 包含跨模組邊界的 bean 引用,如果這些其他模組未包含在測試執行中(有關詳細資訊,請參閱引導模式),則引導將失敗。雖然自然的反應可能是擴充套件所包含的應用程式模組的範圍,但通常更好的選擇是模擬目標 bean。
-
Java
-
Kotlin
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockitoBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockitoBean SomeOtherComponent someOtherComponent
}
Spring Boot 將為定義為 @MockitoBean 的型別建立 bean 定義和例項,並將它們新增到為測試執行引導的 ApplicationContext 中。
如果您發現您的應用程式模組依賴於太多其他模組的 bean,這通常是模組之間高度耦合的跡象。應審查依賴項,以確定它們是否可以透過釋出領域事件來替換。
定義整合測試場景
整合測試應用程式模組可能是一項相當複雜的工作。特別是如果它們的整合基於非同步事務性事件處理,處理併發執行可能會出現細微的錯誤。此外,它還需要處理相當多的基礎設施元件:TransactionOperations 和 ApplicationEventProcessor 以確保事件被髮布並傳遞給事務性監聽器,Awaitility 以處理併發性,以及 AssertJ 斷言以對測試執行結果形成預期。
為了簡化應用程式模組整合測試的定義,Spring Modulith 提供了 Scenario 抽象,可以透過在宣告為 @ApplicationModuleTest 的測試中將其宣告為測試方法引數來使用。
Scenario API-
Java
-
Kotlin
@ApplicationModuleTest
class SomeApplicationModuleTest {
@Test
public void someModuleIntegrationTest(Scenario scenario) {
// Use the Scenario API to define your integration test
}
}
@ApplicationModuleTest
class SomeApplicationModuleTest {
@Test
fun someModuleIntegrationTest(scenario: Scenario) {
// Use the Scenario API to define your integration test
}
}
測試定義本身通常遵循以下骨架
-
定義對系統的刺激。這通常是事件釋出或模組暴露的 Spring 元件的呼叫。
-
可選地自定義執行的技術細節(超時等)。
-
定義一些預期的結果,例如另一個符合某些標準的應用程式事件被觸發,或者模組的某些狀態透過呼叫暴露的元件可以檢測到。
-
對接收到的事件或觀察到的更改狀態進行可選的附加驗證。
Scenario 暴露了一個 API 來定義這些步驟並指導您完成定義。
Scenario 的起點-
Java
-
Kotlin
// Start with an event publication
scenario.publish(new MyApplicationEvent(…)).…
// Start with a bean invocation
scenario.stimulate(() -> someBean.someMethod(…)).…
// Start with an event publication
scenario.publish(MyApplicationEvent(…)).…
// Start with a bean invocation
scenario.stimulate(Runnable { someBean.someMethod(…) }).…
事件釋出和 bean 呼叫都將發生在事務回撥中,以確保給定事件或在 bean 呼叫期間釋出的任何事件都將傳遞給事務性事件監聽器。請注意,這將需要啟動一個**新**事務,無論測試用例是否已在事務中執行。換句話說,由刺激觸發的資料庫狀態更改將**永遠不會**回滾,必須手動清理。為此請參閱 ….andCleanup(…) 方法。
現在可以透過泛型 ….customize(…) 方法或用於常見用例的專用方法(例如設定超時(….waitAtMost(…)))來自定義執行。
設定階段將透過定義刺激結果的實際預期來結束。這反過來可能是一個特定型別的事件,可選地透過匹配器進一步約束。
-
Java
-
Kotlin
….andWaitForEventOfType(SomeOtherEvent.class)
.matching(event -> …) // Use some predicate here
.…
….andWaitForEventOfType(SomeOtherEvent.class)
.matching(event -> …) // Use some predicate here
.…
這些行設定了一個完成標準,最終執行將等待它繼續。換句話說,上面的示例將導致執行最終阻塞,直到達到預設超時或釋出了與定義謂詞匹配的 SomeOtherEvent。
執行基於事件的 Scenario 的終止操作名為 ….toArrive…(),並允許選擇性地訪問預期釋出的事件,或原始刺激中定義的 bean 呼叫的結果物件。
-
Java
-
Kotlin
// Executes the scenario
….toArrive(…)
// Execute and define assertions on the event received
….toArriveAndVerify(event -> …)
// Executes the scenario
….toArrive(…)
// Execute and define assertions on the event received
….toArriveAndVerify(event -> …)
單獨看這些步驟時,方法名稱可能看起來有點奇怪,但它們結合起來讀起來非常流暢。
Scenario 定義-
Java
-
Kotlin
scenario.publish(new MyApplicationEvent(…))
.andWaitForEventOfType(SomeOtherEvent.class)
.matching(event -> …)
.toArriveAndVerify(event -> …);
scenario.publish(new MyApplicationEvent(…))
.andWaitForEventOfType(SomeOtherEvent::class.java)
.matching { event -> … }
.toArriveAndVerify { event -> … }
除了作為預期完成訊號的事件釋出之外,我們還可以透過呼叫其中一個暴露元件的方法來檢查應用程式模組的狀態。場景將更像這樣
-
Java
-
Kotlin
scenario.publish(new MyApplicationEvent(…))
.andWaitForStateChange(() -> someBean.someMethod(…)))
.andVerify(result -> …);
scenario.publish(MyApplicationEvent(…))
.andWaitForStateChange { someBean.someMethod(…) }
.andVerify { result -> … }
傳遞給 ….andVerify(…) 方法的 result 將是方法呼叫返回的值,以檢測狀態變化。預設情況下,非 null 值和非空 Optional 將被視為確定的狀態變化。這可以透過使用 ….andWaitForStateChange(…, Predicate) 過載來調整。
自定義場景執行
要自定義單個場景的執行,請在 Scenario 的設定鏈中呼叫 ….customize(…) 方法。
Scenario 執行-
Java
-
Kotlin
scenario.publish(new MyApplicationEvent(…))
.customize(conditionFactory -> conditionFactory.atMost(Duration.ofSeconds(2)))
.andWaitForEventOfType(SomeOtherEvent.class)
.matching(event -> …)
.toArriveAndVerify(event -> …);
scenario.publish(MyApplicationEvent(…))
.customize { it.atMost(Duration.ofSeconds(2)) }
.andWaitForEventOfType(SomeOtherEvent::class.java)
.matching { event -> … }
.toArriveAndVerify { event -> … }
要全域性自定義測試類的所有 Scenario 例項,請實現 ScenarioCustomizer 並將其註冊為 JUnit 擴充套件。
ScenarioCustomizer-
Java
-
Kotlin
@ExtendWith(MyCustomizer.class)
class MyTests {
@Test
void myTestCase(Scenario scenario) {
// scenario will be pre-customized with logic defined in MyCustomizer
}
static class MyCustomizer implements ScenarioCustomizer {
@Override
Function<ConditionFactory, ConditionFactory> getDefaultCustomizer(Method method, ApplicationContext context) {
return conditionFactory -> …;
}
}
}
@ExtendWith(MyCustomizer::class)
class MyTests {
@Test
fun myTestCase(scenario: Scenario) {
// scenario will be pre-customized with logic defined in MyCustomizer
}
class MyCustomizer : ScenarioCustomizer {
override fun getDefaultCustomizer(method: Method, context: ApplicationContext): UnaryOperator<ConditionFactory> {
return UnaryOperator { conditionFactory -> … }
}
}
}
變更感知測試執行
從版本 1.3 開始,Spring Modulith 附帶了一個 JUnit Jupiter 擴充套件,它將最佳化測試的執行,從而跳過不受專案更改影響的測試。要啟用此最佳化,請在測試範圍中將 spring-modulith-junit 構件宣告為依賴項。
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-junit</artifactId>
<scope>test</scope>
</dependency>
如果測試位於根模組、已發生更改的模組或傳遞依賴於已發生更改的模組中,則將選擇它們執行。在以下情況下,最佳化將放棄最佳化執行
-
測試執行源自 IDE,因為我們假設執行是顯式觸發的。
-
更改集包含對與構建系統相關的資源的更改(
pom.xml、build.gradle(.kts)、gradle.properties和settings.gradle(.kts))。 -
更改集包含對任何類路徑資源的更改。
-
專案根本沒有更改(CI 構建中常見)。
要在 CI 環境中最佳化執行,您需要填充spring.modulith.test.reference-commit 屬性,指向上次成功構建的提交,並確保構建檢出所有提交直到引用提交。然後,檢測應用程式模組更改的演算法將考慮該增量中所有已更改的檔案。要覆蓋專案修改檢測,請透過spring.modulith.test.file-modification-detector 屬性宣告 FileModificationDetector 的實現。