整合測試應用程式模組
Spring Modulith 允許執行整合測試,以隔離或與其他模組組合的方式引導個別應用程式模組。為了實現這一點,請將 Spring Modulith 測試啟動器添加到您的專案中,如下所示
<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 {
@MockBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockBean SomeOtherComponent someOtherComponent
}
Spring Boot 將為定義為 @MockBean
的類型創建 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 -> … }
}
}
}