对于最新的稳定版本,请使用 Spring Modulith 1.4.3spring-doc.cadn.net.cn

集成测试应用程序模块

Spring Modulith 允许运行集成测试,单独或与其他模块组合引导单个应用程序模块。为此,请像这样将 Spring Modulith 测试Starters添加到您的项目中spring-doc.cadn.net.cn

<dependency>
	<groupId>org.springframework.modulith</groupId>
	<artifactId>spring-modulith-starter-test</artifactId>
	<scope>test</scope>
</dependency>

并将 JUnit 测试类放在应用程序模块包或该包的任何子包中,并使用@ApplicationModuleTest:spring-doc.cadn.net.cn

应用模块集成测试类
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.modulithDEBUG,您将看到有关测试执行如何自定义 Spring Boot 引导程序的详细信息:spring-doc.cadn.net.cn

应用程序模块集成测试引导的日志输出
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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.

请注意,输出如何包含有关测试运行中包含的模块的详细信息。它创建应用程序模块,找到要运行的模块,并将自动配置、组件和实体扫描的应用程序限制为相应的包。spring-doc.cadn.net.cn

引导模式

应用模块测试可以在多种模式下引导:spring-doc.cadn.net.cn

处理传出依赖关系

当一个应用程序模块被引导时,它包含的 Spring Bean 将被实例化。 如果这些模块包含跨模块边界的 bean 引用,那么如果测试运行中未包含这些其他模块,则引导将失败(有关详细信息,请参阅引导模式)。 虽然自然的反应可能是扩展所包含的应用程序模块的范围,但模拟目标 Bean 通常是更好的选择。spring-doc.cadn.net.cn

模拟其他应用程序模块中的 Spring bean 依赖项
@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockBean SomeOtherComponent someOtherComponent
}

Spring Boot 将为定义为@MockBean并将它们添加到ApplicationContext引导测试运行。spring-doc.cadn.net.cn

如果您发现您的应用程序模块依赖于其他模块的太多 bean,这通常是它们之间高度耦合的标志。 应检查依赖项是否是通过发布域事件进行替换的候选项。spring-doc.cadn.net.cn

定义集成测试场景

集成测试应用程序模块可能成为一项相当复杂的工作。 特别是如果这些的集成基于异步事务事件处理,则处理并发执行可能会受到细微错误的影响。 此外,它还需要处理相当多的基础设施组件:TransactionOperationsApplicationEventProcessor确保事件发布并交付给事务侦听器,Awaitility 处理并发,AssertJ 断言制定对测试执行结果的期望。spring-doc.cadn.net.cn

为了简化应用程序模块集成测试的定义,Spring Modulith 提供了Scenario抽象,可以通过在声明为@ApplicationModuleTest.spring-doc.cadn.net.cn

使用ScenarioJUnit 5 测试中的 API
@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-doc.cadn.net.cn

  1. 定义了对系统的刺激。这通常是事件发布或模块公开的 Spring 组件的调用。spring-doc.cadn.net.cn

  2. 可选自定义执行的技术细节(超时等)spring-doc.cadn.net.cn

  3. 某些预期结果的定义,例如触发的另一个与某些条件匹配的应用程序事件,或者可以通过调用公开的组件来检测模块的某些状态更改。spring-doc.cadn.net.cn

  4. 可选的、对接收到的事件或观察到的更改状态进行的其他验证。spring-doc.cadn.net.cn

Scenario公开了一个 API 来定义这些步骤并指导您完成定义。spring-doc.cadn.net.cn

将刺激定义为Scenario
// 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(…)为此目的的方法。spring-doc.cadn.net.cn

生成的对象现在可以通过泛型….customize(…)方法或专门用于常见用例的方法,例如设置超时 (….waitAtMost(…)).spring-doc.cadn.net.cn

设置阶段将通过定义对刺激结果的实际预期来结束。 这可以是特定类型的事件,可以选择由匹配器进一步约束:spring-doc.cadn.net.cn

期望将事件发布为作结果
….andWaitForEventOfType(SomeOtherEvent.class)
 .matching(event -> …) // Use some predicate here
 .…
….andWaitForEventOfType(SomeOtherEvent.class)
 .matching(event -> …) // Use some predicate here
 .…

这些行设置了最终执行将等待继续的完成标准。 换句话说,上面的示例将导致执行最终阻塞,直到达到默认超时或SomeOtherEvent已发布与定义的谓词匹配的命令。spring-doc.cadn.net.cn

执行基于事件的终端作Scenario被命名为….toArrive…()并允许选择性地访问已发布的预期事件,或原始激励中定义的 Bean 调用的结果对象。spring-doc.cadn.net.cn

触发验证
// 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 -> …)

单独查看步骤时,方法名称的选择可能看起来有点奇怪,但组合起来时,它们实际上读起来非常流畅。spring-doc.cadn.net.cn

一个完整的Scenario定义
scenario.publish(new MyApplicationEvent(…))
  .andWaitForEventOfType(SomeOtherEvent.class)
  .matching(event -> …)
  .toArriveAndVerify(event -> …);
scenario.publish(new MyApplicationEvent(…))
  .andWaitForEventOfType(SomeOtherEvent::class.java)
  .matching { event -> … }
  .toArriveAndVerify { event -> … }

除了充当预期完成信号的事件发布之外,我们还可以通过在公开的组件之一上调用方法来检查应用程序模块的状态。 然后,该场景看起来像这样:spring-doc.cadn.net.cn

期待状态更改
scenario.publish(new MyApplicationEvent(…))
  .andWaitForStateChange(() -> someBean.someMethod(…)))
  .andVerify(result -> …);
scenario.publish(MyApplicationEvent(…))
  .andWaitForStateChange { someBean.someMethod(…) }
  .andVerify { result -> … }

result交给….andVerify(…)method 将是方法调用返回的值,以检测状态变化。默认情况下,非null值和非空Optionals 将被视为决定性的状态更改。这可以通过使用….andWaitForStateChange(…, Predicate)超载。spring-doc.cadn.net.cn

自定义方案执行

要自定义单个方案的执行,请调用….customize(…)方法的设置链中的Scenario:spring-doc.cadn.net.cn

自定义Scenario执行
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 扩展。spring-doc.cadn.net.cn

注册ScenarioCustomizer
@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 -> … }
    }
  }
}