对于最新稳定版本,请使用 Spring Modulith 2.0.4spring-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.modulith 的日志级别配置为 DEBUG,您将看到有关测试执行如何自定义 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 进行模拟(Mock)。spring-doc.cadn.net.cn

在其他应用程序模块中模拟 Spring Bean 依赖
@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockitoBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockitoBean SomeOtherComponent someOtherComponent
}

Spring Boot 将为定义为 @MockitoBean 的类型创建 Bean 定义和实例,并将它们添加到为测试运行引导的 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

在 JUnit 5 测试中使用 Scenario 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
 .…

这些行设置了完成条件,后续执行将等待该条件满足后才继续。 换句话说,上述示例会导致执行最终阻塞,直到达到默认超时时间,或者发布了一个匹配所定义谓语的 SomeOtherEventspring-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 -> … }

传递给 ….andVerify(…) 方法的 result 将是用于检测状态变更的方法调用所返回的值。 默认情况下,非 null 值和非空的 Optional 将被视为确定性的状态变更。 这可以通过使用 ….andWaitForStateChange(…, Predicate) 重载方法进行调整。spring-doc.cadn.net.cn

自定义场景执行

要自定义单个场景的执行,请在 Scenario 的设置链中调用 ….customize(…) 方法: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 -> … }
    }
  }
}

变更感知测试执行

自 1.3 版本起,Spring Modulith 提供了一个 JUnit Jupiter 扩展,用于优化测试执行,使得未受项目变更影响的测试将被跳过。 要启用该优化,请在测试作用域中将 spring-modulith-junit 构件声明为依赖项:spring-doc.cadn.net.cn

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

如果测试位于根模块、发生过变更的模块,或传递依赖于已发生变更模块的模块中,则会被选中执行。 在以下情况下,优化将停止对执行的优化:spring-doc.cadn.net.cn

为了在 CI 环境中优化执行,您需要填充指向最后一次成功构建提交的 spring.modulith.test.reference-commit 属性,并确保构建检出直到该参考提交的所有提交。 检测应用程序模块变更的算法将随后考虑该差异中所有已变更的文件。 若要覆盖项目修改检测,请通过 spring.modulith.test.file-modification-detector 属性 声明一个 FileModificationDetector 的实现。spring-doc.cadn.net.cn