集成测试应用程序模块
Spring Modulith 允许在隔离或与其他模块组合的情况下引导单个应用程序模块以运行集成测试。为此,请将 Spring Modulith 测试 starter 添加到您的项目中,如下所示
<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— 运行当前模块以及整个依赖模块树。
处理传出依赖
当应用程序模块被引导时,它包含的 Spring bean 将被实例化。如果这些 bean 包含跨模块边界的 bean 引用,如果这些其他模块未包含在测试运行中(有关详细信息,请参阅引导模式),则引导将失败。虽然自然的反应可能是扩展所包含的应用程序模块的范围,但通常更好的选择是模拟目标 bean。
-
Java
-
Kotlin
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockitoBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockitoBean SomeOtherComponent someOtherComponent
}
Spring Boot 将为定义为 @MockitoBean 的类型创建 bean 定义和实例,并将它们添加到为测试运行引导的 ApplicationContext 中。
如果您发现您的应用程序模块依赖于太多其他模块的 bean,这通常是模块之间高度耦合的迹象。应审查依赖项,以确定它们是否可以通过发布领域事件来替换。
定义集成测试场景
集成测试应用程序模块可能是一项相当复杂的工作。特别是如果它们的集成基于异步事务性事件处理,处理并发执行可能会出现细微的错误。此外,它还需要处理相当多的基础设施组件:TransactionOperations 和 ApplicationEventProcessor 以确保事件被发布并传递给事务性监听器,Awaitility 以处理并发性,以及 AssertJ 断言以对测试执行结果形成预期。
为了简化应用程序模块集成测试的定义,Spring Modulith 提供了 Scenario 抽象,可以通过在声明为 @ApplicationModuleTest 的测试中将其声明为测试方法参数来使用。
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
}
}
测试定义本身通常遵循以下骨架
-
定义对系统的刺激。这通常是事件发布或模块暴露的 Spring 组件的调用。
-
可选地自定义执行的技术细节(超时等)。
-
定义一些预期的结果,例如另一个符合某些标准的应用程序事件被触发,或者模块的某些状态通过调用暴露的组件可以检测到。
-
对接收到的事件或观察到的更改状态进行可选的附加验证。
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 构件声明为依赖项。
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-junit</artifactId>
<scope>test</scope>
</dependency>
如果测试位于根模块、已发生更改的模块或传递依赖于已发生更改的模块中,则将选择它们执行。在以下情况下,优化将放弃优化执行
-
测试执行源自 IDE,因为我们假设执行是显式触发的。
-
更改集包含对与构建系统相关的资源的更改(
pom.xml、build.gradle(.kts)、gradle.properties和settings.gradle(.kts))。 -
更改集包含对任何类路径资源的更改。
-
项目根本没有更改(CI 构建中常见)。
要在 CI 环境中优化执行,您需要填充spring.modulith.test.reference-commit 属性,指向上次成功构建的提交,并确保构建检出所有提交直到引用提交。然后,检测应用程序模块更改的算法将考虑该增量中所有已更改的文件。要覆盖项目修改检测,请通过spring.modulith.test.file-modification-detector 属性声明 FileModificationDetector 的实现。