Spring Boot 4 Complete Guide · บทเรียน

การทดสอบการผสานรวมโมดูลและสถานการณ์

ทดสอบการโต้ตอบข้ามโมดูลแบบแยกส่วนด้วยการรองรับสถานการณ์และส่วนย่อยการทดสอบของ Modulith

บทเรียน 4 จาก 413 ขั้นตอน

การทดสอบการผสานรวมโมดูลและสถานการณ์ เป็นบทเรียน Spring Boot 4 Complete Guide ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน 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_DEPENDENCIES so 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:

  • @ApplicationModuleTest bootstraps a single module; bootstrap modes (STANDALONE, DIRECT_DEPENDENCIES, ALL_DEPENDENCIES) control how many neighbours load.
  • PublishedEvents / AssertablePublishedEvents capture and assert what a module publishes.
  • The Scenario API models stimulus → expectation for async flows: stimulate(...) or publish(...), then andWaitForEventOfType(...) and toArrive() / toArriveAndVerify(...).
  • andWaitForStateChange(...) plus customize(...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.

เริ่มต้นได้ฟรี

เรียนรู้ Java ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
21
บทเรียน
84

คำถามที่พบบ่อย

บทเรียน “การทดสอบการผสานรวมโมดูลและสถานการณ์” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การทดสอบการผสานรวมโมดูลและสถานการณ์” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Spring Boot 4 Complete Guide ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Spring Boot 4 Complete Guide มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การทดสอบการผสานรวมโมดูลและสถานการณ์”

ทดสอบการโต้ตอบข้ามโมดูลแบบแยกส่วนด้วยการรองรับสถานการณ์และส่วนย่อยการทดสอบของ Modulith คุณปฏิบัติ Spring Boot 4 Complete Guide ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Spring Boot 4 Complete Guide หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Spring Boot 4 Complete Guide บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “การทดสอบการผสานรวมโมดูลและสถานการณ์” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Spring Boot 4 Complete Guide นี้ได้ไหม

ได้ บทเรียน Spring Boot 4 Complete Guide ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. โมดูลแอปพลิเคชันและการตรวจสอบขอบเขต
  2. เหตุการณ์ภายในแอปพลิเคชันและตัวรับฟัง
  3. การเผยแพร่เหตุการณ์เชิงธุรกรรมและเอาต์บ็อกซ์
  4. การทดสอบการผสานรวมโมดูลและสถานการณ์
← กลับไปที่ Spring Boot 4 Complete Guide