整合測試應用模組

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。

模擬其他應用模組中的 Spring Bean 依賴
  • Java

  • Kotlin

@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockitoBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockitoBean SomeOtherComponent someOtherComponent
}

Spring Boot 將為定義為 @MockitoBean 的型別建立 Bean 定義和例項,並將它們新增到為測試執行引導的 ApplicationContext 中。

如果您發現您的應用模組依賴於其他模組中的太多 Bean,這通常是模組之間高耦合的跡象。應該審查這些依賴是否適合透過釋出領域事件來替換。

定義整合測試場景

整合測試應用模組可能會變得相當複雜。特別是如果整合是基於非同步、事務性事件處理,處理併發執行可能會出現細微的錯誤。此外,它還需要處理相當多的基礎設施元件:TransactionOperationsApplicationEventProcessor 以確保事件被髮布並傳遞給事務監聽器,Awaitility 用於處理併發,以及 AssertJ 斷言用於描述測試執行結果的預期。

為了簡化應用模組整合測試的定義,Spring Modulith 提供了 Scenario 抽象,可以在宣告為 @ApplicationModuleTest 的測試中,透過將其宣告為測試方法引數來使用。

在 JUnit 5 測試中使用 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
  }
}

測試定義本身通常遵循以下框架

  1. 定義對系統的刺激。這通常是一個事件釋出或呼叫模組公開的 Spring 元件。

  2. 可選地自定義執行的技術細節(超時等)

  3. 定義一些預期的結果,例如觸發另一個符合特定標準的事件,或者模組透過呼叫公開的元件可以檢測到的某個狀態變化。

  4. 可選地,對接收到的事件或觀察到的變化狀態進行額外驗證。

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.xmlbuild.gradle(.kts)gradle.propertiessettings.gradle(.kts))。

  • 變更集合包含對任何 classpath 資源的變更。

  • 專案根本沒有變更(CI 構建中很典型)。

要在 CI 環境中最佳化執行,您需要填寫spring.modulith.test.reference-commit 屬性,指向上次成功構建的提交,並確保構建檢出所有提交直到參考提交。然後,檢測應用模組變更的演算法將考慮在該差異中更改的所有檔案。要覆蓋專案修改檢測,請透過spring.modulith.test.file-modification-detector 屬性宣告 FileModificationDetector 的實現。