模块集成测试与场景
使用 Modulith 的场景和测试切片支持,隔离测试跨模块交互。
模块集成测试与场景 是 CoddyKit 上的免费 Spring Boot 4 Complete Guide 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Spring Boot 4 Complete Guide 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Spring Boot 4 Complete Guide 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Module Integration Tests?
In an event-driven Modulith, modules talk to each other by publishing and consuming application events rather than calling each other directly. A unit test of a single bean can verify the publishing side, but it cannot prove that the other module actually reacts correctly.
- Unit test — one bean, everything else mocked.
- Module integration test — one module's Spring context, its real beans, but neighbours stubbed.
- Full integration test — the whole application context.
Spring Modulith gives you the middle layer: spin up exactly one module, exercise its event flow, and assert what it publishes — all in isolation.
The @ApplicationModuleTest Slice
The core annotation is @ApplicationModuleTest from spring-modulith-test. It bootstraps only the module that contains the test class, not the entire application.
- It auto-detects the module from the test's package.
- Beans from other modules are not loaded — their events are captured instead of really handled.
- It behaves like a Spring Boot test slice (similar to
@DataJpaTest), so it is fast and focused.
Place the test in the same base package as the module under test so Modulith can resolve module boundaries correctly.
package com.example.shop.order;
import org.springframework.modulith.test.ApplicationModuleTest;
@ApplicationModuleTest
class OrderModuleIntegrationTests {
// Only the 'order' module's beans are bootstrapped here.
// Other modules (inventory, shipping) are NOT loaded.
}Bootstrap Modes
@ApplicationModuleTest accepts a bootstrap mode that controls how many neighbouring modules are pulled in:
STANDALONE(default) — only the current module.DIRECT_DEPENDENCIES— the current module plus the modules it directly depends on.ALL_DEPENDENCIES— the current module and its entire transitive dependency tree.
Start with STANDALONE for the tightest isolation; widen the mode only when a test genuinely needs a real collaborator bean instead of a captured event.
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.ApplicationModuleTest.BootstrapMode;
@ApplicationModuleTest(BootstrapMode.DIRECT_DEPENDENCIES)
class OrderModuleIntegrationTests {
// 'order' + every module it directly depends on are loaded.
}Capturing Published Events
The simplest assertion is: did my module publish the right event? Modulith injects a PublishedEvents instance that records every event published during the test.
- Inject it as a test parameter or autowire it.
- Filter by type with
ofType(...). - Refine with
matching(...)on a field or predicate.
This lets you verify the publishing module's contract without loading any consumer.
@ApplicationModuleTest
class OrderModuleIntegrationTests {
@Autowired OrderService orders;
@Test
void publishesEventOnCompletion(PublishedEvents events) {
orders.complete(new OrderId("4711"));
var completed = events.ofType(OrderCompleted.class)
.matching(OrderCompleted::orderId, new OrderId("4711"));
assertThat(completed).hasSize(1);
}
}AssertablePublishedEvents
For a more fluent, AssertJ-style API, inject AssertablePublishedEvents instead. It exposes assertThat() directly so you can chain expressive assertions.
contains(Type.class)— at least one event of that type.matching(...)— narrow to events whose field equals a value.- Great for readability in larger scenarios.
Both PublishedEvents and AssertablePublishedEvents are populated by the same event-capturing infrastructure.
@Test
void completionPublishesExactlyOnce(AssertablePublishedEvents events) {
orders.complete(new OrderId("4711"));
assertThat(events)
.contains(OrderCompleted.class)
.matching(OrderCompleted::orderId, new OrderId("4711"));
}The Scenario API
Captured events tell you what was published, but event-driven flows are often asynchronous: a stimulus triggers a listener that publishes a follow-up event. The Scenario API models exactly this stimulus → expectation shape, and it transparently waits for async work to settle.
stimulate(...)— the action that kicks off the flow.andWaitForEventOfType(...)— block until that event appears (with a timeout).toArrive()/toArriveAndVerify(...)— the terminal assertion.
Inject Scenario as a test-method parameter; you do not construct it yourself.
Stimulus Triggered by a Method Call
The most common scenario starts with a bean method invocation. Pass a lambda receiving the Scenario, call your service inside stimulate(...), then declare what event you expect downstream.
Modulith polls until the event arrives or the timeout expires — no manual Thread.sleep needed.
@ApplicationModuleTest
class OrderModuleIntegrationTests {
@Autowired OrderService orders;
@Test
void completingOrderTriggersFollowUp(Scenario scenario) {
scenario.stimulate(() -> orders.complete(new OrderId("4711")))
.andWaitForEventOfType(OrderCompleted.class)
.matchingMappedValue(OrderCompleted::orderId, new OrderId("4711"))
.toArrive();
}
}Stimulus Triggered by Publishing an Event
To test the consuming side of a module in isolation, make the stimulus an incoming event rather than a method call. The module's listener reacts to it and (typically) publishes its own result event.
stimulate(event)publishes the inbound event into the context.- You then wait for the module's own outbound event.
This is how you verify one module's reaction to another module's event without loading that other module at all.
@Test
void inventoryReservesStockOnOrder(Scenario scenario) {
scenario.publish(new OrderCompleted(new OrderId("4711")))
.andWaitForEventOfType(StockReserved.class)
.matchingMappedValue(StockReserved::orderId, new OrderId("4711"))
.toArrive();
}Verifying State, Not Just Events
Sometimes the assertion you really care about is a state change — a repository row, a counter, a status flag — rather than another event. toArriveAndVerify(...) runs your assertion once the awaited event has arrived.
You can also use andVerify(...) after waiting for state to stabilise. This bridges the gap between async event delivery and synchronous database checks.
@Test
void reservationPersistsState(Scenario scenario) {
scenario.publish(new OrderCompleted(new OrderId("4711")))
.andWaitForEventOfType(StockReserved.class)
.toArriveAndVerify(event ->
assertThat(reservations.findByOrderId(event.orderId()))
.isPresent());
}Customising Timeouts and State Changes
Async tests need sensible timeouts. The Scenario API lets you tune the wait and even wait on arbitrary state instead of an event:
customize(c -> c.atMost(Duration.ofSeconds(2)))— cap the wait.andWaitForStateChange(() -> repo.findStatus(id))— poll a supplier until it returns a non-null / truthy value.
Prefer waiting for the concrete signal you care about; a generous-but-bounded timeout avoids flaky CI without masking real hangs.
@Test
void statusFlipsToShipped(Scenario scenario) {
scenario.stimulate(() -> orders.complete(new OrderId("4711")))
.customize(it -> it.atMost(Duration.ofSeconds(2)))
.andWaitForStateChange(() -> orders.statusOf(new OrderId("4711")))
.andVerify(status ->
assertThat(status).isEqualTo(OrderStatus.SHIPPED));
}Mocking Out-of-Module Collaborators
In STANDALONE mode, beans from other modules are absent. If your module under test directly autowires a collaborator that lives in another module (not an event-based dependency), the context cannot start. Two clean options:
- Provide a
@MockitoBean(formerly@MockBean) for that collaborator in the test. - Or widen the bootstrap mode to
DIRECT_DEPENDENCIESso the real bean loads.
Favour the mock when you only need to stub a response; favour widening when the real collaboration is the point of the test.
@ApplicationModuleTest
class OrderModuleIntegrationTests {
@MockitoBean PricingClient pricingClient; // bean from another module
@Autowired OrderService orders;
@Test
void usesQuotedPrice(Scenario scenario) {
when(pricingClient.quote(any())).thenReturn(Money.of(42));
scenario.stimulate(() -> orders.place(new Cart("4711")))
.andWaitForEventOfType(OrderPlaced.class)
.toArrive();
}
}Quick Check
You want to test that the inventory module reserves stock when it receives an OrderCompleted event — without loading the order module that normally publishes it. Which approach fits best?
Recap
You learned how Spring Modulith tests cross-module event flows in isolation:
@ApplicationModuleTestbootstraps a single module; bootstrap modes (STANDALONE,DIRECT_DEPENDENCIES,ALL_DEPENDENCIES) control how many neighbours load.PublishedEvents/AssertablePublishedEventscapture and assert what a module publishes.- The
ScenarioAPI models stimulus → expectation for async flows:stimulate(...)orpublish(...), thenandWaitForEventOfType(...)andtoArrive()/toArriveAndVerify(...). andWaitForStateChange(...)pluscustomize(...atMost...)handle state assertions and bounded timeouts.- Stub out-of-module collaborators with
@MockitoBean, or widen the bootstrap mode when the real collaboration matters.
Together these let you prove module contracts hold without spinning up the entire application.
常见问题解答
「模块集成测试与场景」课时是免费的吗?
是的 — 「模块集成测试与场景」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Spring Boot 4 Complete Guide 课程的其余内容,请升级到 CoddyKit PRO。 Spring Boot 4 Complete Guide 课程共包含 4 节课。
「模块集成测试与场景」这节课中我会学到什么?
使用 Modulith 的场景和测试切片支持,隔离测试跨模块交互。 你通过在浏览器中直接运行的动手代码来练习 Spring Boot 4 Complete Guide,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Spring Boot 4 Complete Guide 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Spring Boot 4 Complete Guide 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「模块集成测试与场景」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Spring Boot 4 Complete Guide 课中编写并运行代码吗?
能。每节 Spring Boot 4 Complete Guide 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 应用模块与边界验证
- 应用内部事件与监听器
- 事务型事件发布与发件箱
- 模块集成测试与场景