Read time: ~

Testing RabbitMQ Apps

Integration-test listeners, confirms, and dead-lettering against a real broker with Testcontainers, using Awaitility for async assertions and contract tests for serialization.

Prerequisite:First Producer and ConsumerYou’ll need: Docker running locally and the Spring AMQP services from earlier modules.

Messaging code is asynchronous and spans process boundaries, which makes it easy to write tests that pass by accident. This module shows how to test against a real broker with Testcontainers, assert on async outcomes without flaky sleeps, and pin down the message contract so a serialization change cannot silently break consumers.


What you’ll be able to do after this module

  • Spin up a disposable RabbitMQ broker for integration tests.
  • Assert on asynchronous consumers without arbitrary sleeps.
  • Verify acknowledgement, retry, and dead-letter behavior.
  • Contract-test serialization so producers and consumers stay compatible.
  • Choose the right test level for each concern.

1. The testing pyramid for messaging

Not everything needs a broker. Push logic down to fast unit tests and reserve integration tests for the wiring that only a real broker exercises.

flowchart TD
    U["Unit tests<br/>handler logic, no broker (mock)"]
    I["Integration tests<br/>real broker via Testcontainers"]
    C["Contract tests<br/>serialization / payload shape"]
    U --> I
    I --> C

Unit-test the handler’s business logic by calling it directly. Use integration tests for topology, acks, retries, and dead-lettering, which mocks cannot verify. Contract tests guard the message shape that crosses service boundaries.


2. A real broker with Testcontainers

An embedded fake cannot reproduce real routing, acks, or DLX behavior. Testcontainers starts a genuine broker in Docker for the test and tears it down after.

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>rabbitmq</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.awaitility</groupId>
  <artifactId>awaitility</artifactId>
  <scope>test</scope>
</dependency>

Point Spring at the container with @ServiceConnection, which wires host, port, and credentials automatically.

@SpringBootTest
@Testcontainers
class OrderMessagingIT {

    @Container
    @ServiceConnection
    static RabbitMQContainer rabbit =
            new RabbitMQContainer("rabbitmq:3.13-management");

    @Autowired RabbitTemplate rabbitTemplate;
    @Autowired InventoryHandler handler; // real listener bean
}

No manual property wiring is needed: @ServiceConnection overrides spring.rabbitmq.* for the test context.


3. Asserting on async consumers with Awaitility

The publish returns before the consumer runs, so a plain assert right after it will fail. Never paper over this with Thread.sleep; poll for the expected outcome with a timeout instead.

sequenceDiagram
    participant T as Test
    participant B as RabbitMQ (Testcontainers)
    participant H as Listener
    T->>B: publish OrderCreated
    T->>T: await up to 5s ...
    B->>H: deliver
    H->>H: reserve stock
    T->>T: assertion passes once stock reserved
@Test
void inventoryReservesStockOnOrderCreated() {
    rabbitTemplate.convertAndSend(
            "orders.topic", "order.created",
            new OrderCreated("evt-1", "order-1", "SKU-9", 2));

    await().atMost(Duration.ofSeconds(5))
           .untilAsserted(() ->
               assertThat(handler.reservationsFor("order-1")).isEqualTo(2));
}

Awaitility retries the assertion until it passes or the timeout expires, which removes flakiness without slowing the happy path.


4. Testing acks, retries and dead-lettering

The reliability behavior from Section 4 is exactly what integration tests should verify, because it is invisible to unit tests.

@Test
void poisonMessageLandsInDeadLetterQueue() {
    // handler throws for this payload, exhausting retries
    rabbitTemplate.convertAndSend(
            "orders.topic", "order.created", poisonOrder());

    await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> {
        Object dead = rabbitTemplate.receiveAndConvert("inventory.dlq");
        assertThat(dead).isNotNull();
    });
}

Similar tests confirm a transient failure is retried and eventually succeeds, and that a successful message is acked and does not reappear. This locks down the guarantees from Retry and Error Handling and Dead Letter Exchanges.


5. Contract-testing serialization

A producer and consumer that deploy separately can drift apart. A contract test pins the on-the-wire JSON so a rename or type change fails a build, not production.

@Test
void orderCreatedSerializesToStableJson() {
    Jackson2JsonMessageConverter converter =
            new Jackson2JsonMessageConverter();
    Message msg = converter.toMessage(
            new OrderCreated("evt-1", "order-1", "SKU-9", 2),
            new MessageProperties());

    assertThatJson(new String(msg.getBody()))
            .isEqualTo("""
                {"eventId":"evt-1","orderId":"order-1",
                 "sku":"SKU-9","qty":2}""");
}

Round-trip tests (serialize then deserialize back to the DTO) catch converter and type-mapping mistakes, echoing the trusted-packages setup from First Producer and Consumer. Keep contract tests fast by running them without a broker.


6. Keeping the suite fast and reliable

  • Reuse the container across tests. Starting a broker per test is slow; a static @Container in a shared base class starts it once per run.
  • Isolate state. Use unique queue or routing-key names per test, or purge queues in a teardown, so tests do not leak into each other.
  • Prefer awaiting over fixed waits. Every sleep is either flaky or slow; Awaitility is both faster and more reliable.
  • Fail fast on timeouts. A tight atMost surfaces a broken consumer quickly instead of hanging the build.

Checkpoint

You should now be able to:

  • Run integration tests against a real broker with Testcontainers.
  • Wire the container into Spring with @ServiceConnection.
  • Assert on async consumers using Awaitility, not sleeps.
  • Verify acks, retries, and dead-lettering end to end.
  • Contract-test serialization to prevent producer/consumer drift.

Next: the Operations and Troubleshooting section.