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
}
这将运行您的集成测试,类似于所实现的测试,但引导程序实际上仅限于测试所在的应用程序模块。
如果将日志级别配置为 ,您将看到有关测试执行如何自定义 Spring Boot 引导程序的详细信息:@SpringBootTest
org.springframework.modulith
DEBUG
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: 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.
请注意,输出如何包含有关测试运行中包含的模块的详细信息。 它创建应用程序模块模块,查找要运行的模块,并将自动配置、组件和实体扫描的应用程序限制在相应的包中。
Bootstrap 模式
应用程序模块测试可以在多种模式下进行引导:
-
STANDALONE
(default) — 仅运行当前模块。 -
DIRECT_DEPENDENCIES
— 运行当前模块以及当前模块直接依赖的所有模块。 -
ALL_DEPENDENCIES
— 运行当前模块和所依赖的整个模块树。
处理 Efferent 依赖关系
当一个应用程序模块被引导时,它所包含的 Spring bean 将被实例化。 如果这些模块包含跨模块边界的 Bean 引用,那么如果测试运行中未包含这些其他模块,则引导程序将失败(有关详细信息,请参阅引导模式)。 虽然自然的反应可能是扩大所包含的应用程序模块的范围,但模拟目标 Bean 通常是更好的选择。
-
Java
-
Kotlin
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockBean SomeOtherComponent someOtherComponent
}
Spring Boot 将为定义为的类型创建 Bean 定义和实例,并将它们添加到测试运行的引导中。@MockBean
ApplicationContext
如果你发现你的应用程序模块依赖于太多其他的 bean,这通常是它们之间高度耦合的迹象。 应检查依赖项,以确定它们是否是发布域事件替换的候选项。
定义集成测试方案
集成测试应用程序模块可能会成为一项非常复杂的工作。
特别是如果这些事件的集成基于异步事务事件处理,则处理并发执行可能会遇到细微的错误。
此外,它还需要处理相当多的基础设施组件:确保事件发布并交付给事务侦听器,Awaitility 处理并发性,AssertJ 断言来制定对测试执行结果的期望。TransactionOperations
ApplicationEventProcessor
为了简化应用程序模块集成测试的定义,Spring Modulith 提供了抽象,可以通过在声明为 的测试中将其声明为测试方法参数来使用。Scenario
@ApplicationModuleTest
Scenario
-
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(() -> 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
.…
这些行设置了一个完成标准,最终执行将等待该标准继续执行。
换言之,上面的示例将导致执行最终阻塞,直到达到默认超时或发布与定义的谓词匹配的 a。SomeOtherEvent
执行基于事件的终端操作被命名,并允许选择性地访问发布的预期事件,或原始刺激中定义的 Bean 调用的结果对象。Scenario
….toArrive…()
-
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 -> …)
传入方法的将是方法调用返回的值,用于检测状态更改。
默认情况下,非值和非空 s 将被视为决定性的状态更改。
这可以通过使用重载来调整。result
….andVerify(…)
null
Optional
….andWaitForStateChange(…, Predicate)
自定义方案执行
若要自定义单个方案的执行,请调用 :….customize(…)
Scenario
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 -> …)
要全局自定义测试类的所有实例,请实现 JUnit 扩展并将其注册为 JUnit 扩展。Scenario
ScenarioCustomizer
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 -> …
}
}
}