整合測試應用模組
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
——運行當前模組及其所有依賴的模組樹。
處理出向依賴(Efferent 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
artifact 依賴
<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)
)。 -
變更集合包含對任何 classpath 資源的變更。
-
專案根本沒有變更(CI 構建中很典型)。
要在 CI 環境中最佳化執行,您需要填寫spring.modulith.test.reference-commit
屬性,指向上次成功構建的提交,並確保構建檢出所有提交直到參考提交。然後,檢測應用模組變更的演算法將考慮在該差異中更改的所有檔案。要覆蓋專案修改檢測,請透過spring.modulith.test.file-modification-detector
屬性宣告 FileModificationDetector
的實現。