skip to content

How is the consumer side of a Pact message pact written when there is no request and no response?

level: middleimportance: should knowfreq 48%

answer

  1. There is nothing to call, so what runs?
  2. The unit under test moves after delivery
  3. The builder declares, the handler consumes
  4. Assert on the domain outcome, not the body
  5. No pact file unless the handler passed

basics

~20 s

The consumer's message handler replaces the HTTP client as the unit under test. The test declares the expected message with a builder, receives the recorded contents, and passes them to the real handler. The pact is written only if that passes.

solid answer

~40 s

There is no mock provider to call, so the **handler** is what the test drives. In Pact-JVM the test is marked asynchronous, a message-pact builder declares a description that names the interaction, an optional provider state, the expected contents and message metadata, and matchers relax generated fields to types or regexes. The framework injects the built message into the test body; the test hands its contents to the production handler and asserts on the domain outcome. The pact file is written only when that assertion passes, which is the point: a recorded expectation must be a *consequence* of running real consumer code. A test that asserts the builder's own body back at itself records a wish list the provider is then obliged to satisfy forever.

code

java · 22 lines
java
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "donation-scheduler", providerType = ProviderType.ASYNCH)
class SlotBookedConsumerTest {

    @Pact(consumer = "donor-reminder-service")
    MessagePact slotBooked(MessagePactBuilder builder) {
        return builder
            .expectsToReceive("a slot-booked event for donor 40817")
            .withContent(new PactDslJsonBody()
                .stringType("donorId", "40817")
                .stringType("centreCode", "BR-217"))
            .toPact();
    }

    @Test
    void handlerDecodesSlotBooked(List<Message> messages) {
        Reminder reminder = new SlotBookedHandler()
            .handle(messages.get(0).contentsAsString());

        assertEquals("40817", reminder.donorId());
    }
}

go deeper

for a junior

Recall that a message contract test runs entirely in memory and that the consumer's listener code, not a network client, is what gets called. You are not expected to write one yet.

for a middle

Be ready to walk the builder inputs — description, provider state, contents, metadata — and explain why matchers replace literal values for generated fields.

for a senior

Show that you police what enters the pact: every recorded field should be one the handler demonstrably reads, because each one becomes an obligation on the producer that is hard to withdraw later.

for a principal

Own the direction-of-authorship decision across teams: consumer-recorded expectations versus producer-authored message contracts change who is blocked by whom, and that is an organisational choice, not a tooling preference.

## Who plays "the consumer" when there is no request In an HTTP pact the consumer is an HTTP client and Pact stands up a local mock provider for it to call. In an asynchronous message pact there is nothing to call. The party being tested is the piece of consumer code that would run *after* delivery: the handler, listener, or deserialize-and-process function that turns bytes into a domain object and does something with it. That substitution is the whole trick. Pact does not simulate a broker, does not create a subscription and does not deliver anything. It builds the payload you declared and hands it to your test, and your test calls the handler with it. The handler is the unit under test; the pact file is a by-product of that unit test passing. ## What the test actually drives, step by step 1. **Declare the pair and the mode.** In Pact-JVM the test is marked as asynchronous rather than HTTP, so the extension knows not to start a mock HTTP server. 2. **Build the expected message.** A message-pact builder takes a description that uniquely names the interaction, an optional provider state, the expected contents, and optionally message metadata such as a content type. 3. **Relax the expectation with matchers.** Fields are pinned by type or regex rather than by literal value wherever the value is generated. 4. **Receive the recorded message in the test body.** The framework injects the built message into the test method. 5. **Invoke the real handler.** The test passes the message contents to the production handler and asserts on the domain outcome — an entity saved, a command emitted, a value returned. 6. **The pact is written only if the test passes.** A failing handler means no pact file, which is the correct outcome: the consumer has no expectation it can honour. ## Matchers matter more here than in HTTP Message payloads are dense with generated values — identifiers, timestamps, correlation keys. If the consumer records literals, the provider's verification will fail the moment its code produces a different but perfectly valid value, and the team will start "fixing" pacts by loosening them under pressure. Pin the *shape*: - **Type matchers** for identifiers and free text, so any string of the right type satisfies it. - **Regex matchers** where format genuinely matters, such as a centre code or an ISO timestamp. - **Literal values only where the value itself is part of the contract**, such as an event-type discriminator the handler switches on. The example values you supply are still important: they are what the consumer's handler is actually run against, so they must be realistic enough to exercise the parsing you care about. ## The mistake that makes the whole test worthless The most common defect in a message consumer test is a self-fulfilling one: the test asserts that the message body produced by the builder contains the fields the builder just put there, and never calls the handler. That records an expectation nobody consumes. The pact becomes a wish list — every field in it is now something the producer must keep emitting forever, even though no consumer code reads it. The rule is simple: **a recorded expectation must be a consequence of running real consumer code**, so if the handler stops reading a field, the field should stop appearing in the pact. ## Against the HTTP shape | | HTTP pact | Asynchronous message pact | | --- | --- | --- | | What Pact stands up | a local mock provider | nothing at all | | What the test drives | your client against the mock | your handler, directly | | What is recorded | request and response | description, contents, metadata, states | | Failure signal | client sent an unexpected request | handler threw or produced the wrong result | ## A note on the producer-first alternative Spring Cloud Contract inverts the direction: the producer writes the message contract, and on the consumer side Stub Runner triggers the labelled message and pushes it into the consumer's listener. The consumer still ends up exercising its real handler against a payload it did not invent, but the artefact is authored by the producer rather than recorded by the consumer, and the consumer's expectations are therefore not fed back to the producer. ## In an interview Say the handler is the unit under test, describe the builder's four inputs, and be explicit that the assertion is on the handler's outcome, not on the payload you just constructed. Then name the self-fulfilling-pact failure mode without being asked — it is the difference between someone who has written one and someone who has read about them.

  • Why should the consumer test assert on the handler's result rather than on the message body?
    Because asserting on the body only re-checks what the builder just constructed, which cannot fail. Asserting on the handler's result ties every recorded field to consumer code that actually reads it, so an unused field naturally falls out of the pact instead of becoming a permanent obligation on the producer.
  • What happens to the pact file if the consumer's handler throws?
    The test fails and no pact is written for that run, so nothing is published. That is the desired behaviour: the consumer has not demonstrated it can honour the expectation, and publishing it anyway would ask the producer to satisfy a contract the consumer cannot meet.

saying these in an interview costs you the question

  • Asserts on the builder's body instead of the handler
  • Expects Pact to start a broker or a subscription
  • Records literal timestamps and generated identifiers
  • Puts fields in the pact that no handler reads
  • Thinks a message pact needs a mock HTTP server