集成测试应用程序模块

Spring Modulith 允许您运行集成测试,以独立或与其他模块组合的方式引导单个应用程序模块。为此,请将 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 - 运行当前模块和依赖的整个模块树。

处理传出依赖项

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

在其他应用程序模块中模拟 Spring bean 依赖项
  • Java

  • Kotlin

@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockBean SomeOtherComponent someOtherComponent
}

Spring Boot 将为定义为 @MockBean 的类型创建 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(() -> 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)
  .matching(event -> …)
  .toArriveAndVerify(event -> …)

除了将事件发布作为预期的完成信号之外,我们还可以通过调用公开的组件之一上的方法来检查应用程序模块的状态。在这种情况下,场景看起来更像这样

预期状态更改
  • Java

  • Kotlin

scenario.publish(new MyApplicationEvent(…))
  .andWaitForStateChange(() -> someBean.someMethod(…)))
  .andVerify(result -> …);
scenario.publish(new MyApplicationEvent(…))
  .andWaitForStateChange(() -> someBean.someMethod(…)))
  .andVerify(result -> …)

传递给….andVerify(…)方法的result将是方法调用返回的值,用于检测状态更改。默认情况下,非null值和非空Optional将被视为决定性的状态更改。这可以通过使用….andWaitForStateChange(…, Predicate)重载来调整。

自定义场景执行

要自定义单个场景的执行,请在Scenario的设置链中调用….customize(…)方法

自定义Scenario执行
  • Java

  • Kotlin

scenario.publish(new MyApplicationEvent(…))
  .customize(it -> it.atMost(Duration.ofSeconds(2)))
  .andWaitForEventOfType(SomeOtherEvent.class)
  .matching(event -> …)
  .toArriveAndVerify(event -> …);
scenario.publish(MyApplicationEvent(…))
  .customize(it -> it.atMost(Duration.ofSeconds(2)))
  .andWaitForEventOfType(SomeOtherEvent::class)
  .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 it -> …;
    }
  }
}
@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) : Function<ConditionFactory, ConditionFactory> {
      return it -> …
    }
  }
}