集成测试应用模块

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 的实现。