Integration Testing Application Modules
Spring Modulith 允许运行集成测试来孤立或与其他测试相结合地引导单个应用程序模块。为此,将 JUnit 测试类放在应用程序模块包或该类的任何子包中,并使用`@ApplicationModuleTest`对其进行注解:
-
Java
-
Kotlin
@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.*
请注意,输出如何包含有关包含在测试运行中的模块的详细信息。它创建应用程序模块模块,查找要运行的模块,并将自动配置、组件和实体扫描的应用限制到相应的包。
Bootstrap Modes
应用程序模块测试可以用各种模式引导:
-
STANDALONE
(默认)——只运行当前模块。 -
DIRECT_DEPENDENCIES
——运行当前模块以及当前模块直接依赖的所有模块。 -
ALL_DEPENDENCIES
——运行当前模块和依赖的模块的整个树。
Dealing with Efferent Dependencies
当启动应用程序模块时,其中包含的 Spring Bean 将被实例化。如果这些 Bean 包含跨越模块边界的 Bean 引用,则当这些其他模块未包含在测试运行中时,启动将会失败(有关详细信息,参见 Bootstrap Modes)。虽然自然反应可能是扩展所包含应用程序模块的范围,但通常更好的选择是对目标 Bean 进行模拟。
-
Java
-
Kotlin
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockBean SomeOtherComponent someOtherComponent
}
Spring Boot 将为定义为`@MockBean`的类型创建 Bean 定义和实例,并将它们添加到为测试运行引导的`ApplicationContext`中。
如果你发现你的应用程序模块依赖于太多其他 Bean,这通常表明它们之间耦合度很高。应该检查依赖项是否有资格通过发布 domain events 来替代。
Defining Integration Test Scenarios
集成测试应用程序模块可能会变成一个非常精细的工作。特别是如果这些集成基于 asynchronous, transactional event handling,处理并发执行可能会出现微妙的错误。此外,还需要处理相当多的基础设施组件: TransactionOperations
和 ApplicationEventProcessor
以确保发布事件并将其传递给事务监听器,Awaitility 来处理并发,以及 AssertJ 断言来制定对测试执行结果的期望。
为了简化应用模块集成测试的定义,Spring Modulith 提供了 Scenario
抽象,可通过将其声明为 @ApplicationModuleTest
中声明的测试中的测试方法参数来使用。
Scenario
API in a JUnit 5 test-
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
.…
这些行设置了完成标准,最终执行将等待其完成。换句话说,上面的示例将导致执行最终阻塞,直到达到默认超时或发布了符合定义的谓词的 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
definition-
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)
重载来调整。
Customizing Scenario Execution
要自定义某个单个场景的执行,请在 Scenario
的设置链中调用 ….customize(…)
方法:
Scenario
execution-
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 -> …
}
}
}