Testing Asynchronous Interactions

An asynchronous interaction is fire-and-forget: a message travels one way and there is no reply. Either your service publishes an event, or it consumes a command.

This guide covers both directions. Examples use Kafka. The same test cases work over other types such as Amazon SNS and SQS, by swapping the transport plugin.

For interactions where a reply comes back, see Testing Request/Reply Interactions.

Which Direction Are You Testing

Your service

AsyncAPI action

Drift's role

Mode

Publishes an event

send

Subscribe and capture

async-observe

Consumes a command

receive

Publish, then check the result

async-inject or async-inject-capture

AsyncAPI 3 actions describe your application, so Drift's role is always the opposite of your service's.

Verifying an Event Your Service Publishes

Use this when your service emits an event and you want to confirm the event matches the contract.

sequenceDiagram
    autonumber
    participant D as Drift
    participant B as Kafka
    participant S as Your service

    D->>B: Subscribe to drift.examples.order-created
    Note over D,B: Drift subscribes first
    D->>S: Run the trigger hook
    S->>B: Publish order.created
    B-->>D: Deliver the matching message
    D->>D: Validate payload and headers<br/>against the AsyncAPI schema
  1. Describe the Operation

    The operation has action: send, because your application is the publisher. Declare a correlationId location so Drift can identify the message.

    asyncapi: 3.1.0
    info:
      title: Order service
      version: 1.0.0
    defaultContentType: application/json
    
    servers:
      local:
        host: localhost:9092
        protocol: kafka
    
    channels:
      orderCreated:
        address: drift.examples.order-created
        messages:
          orderCreated:
            $ref: "#/components/messages/orderCreated"
    
    operations:
      sendOrderCreated:
        action: send
        channel:
          $ref: "#/channels/orderCreated"
        messages:
          - $ref: "#/channels/orderCreated/messages/orderCreated"
    
    components:
      messages:
        orderCreated:
          name: orderCreated
          correlationId:
            location: "$message.header#/correlation-id"
          headers:
            type: object
            properties:
              correlation-id:
                type: string
            required: [correlation-id]
          payload:
            type: object
            required: [eventType, orderId, customerId, status]
            properties:
              eventType:
                type: string
                const: order.created
              orderId:
                type: string
              customerId:
                type: string
              status:
                type: string
                const: created
  2. Write a Trigger

    The trigger causes your service to publish. It must carry the same correlation ID Drift is filtering on.

    trigger:
      executable-type: command
      value: curl
      parameters:
        args:
          - "-X"
          - "POST"
          - "http://localhost:8080/orders"
          - "-H"
          - "x-correlation-id: ${parameters.correlation-id}"
          - "-d"
          - '{"customerId":"CUST-9"}'
      timeout-ms: 3000

    See Using Triggers and Probes for the full hook reference.

  3. Write the Test Case

    # yaml-language-server: $schema=https://download.pactflow.io/drift/schemas/drift.testcases.v1.schema.json
    drift-testcase-file: v1
    title: "Order service outbound events"
    
    sources:
      - name: async-send
        path: ../asyncapi/send.asyncapi.yaml
    
    plugins:
      - name: asyncapi
      - name: kafka
      - name: json
    
    operations:
      SendOrderCreated_Observe:
        target: async-send:sendOrderCreated:orderCreated
        description: "Observe the order.created event emitted after the trigger runs"
        parameters:
          timeout-ms: 5000
          correlation-id: send-order-created-001
          orderId: ORD-100
          customerId: CUST-9
        trigger:
          executable-type: command
          value: python3
          parameters:
            args:
              - ../hooks/trigger-send.py
              - --correlation-id
              - ${parameters.correlation-id}
              - --order-id
              - ${parameters.orderId}
              - --customer-id
              - ${parameters.customerId}
          timeout-ms: 2000
        expected:
          headers:
            correlation-id: ${parameters.correlation-id}
          payload:
            eventType: order.created
            orderId: ${parameters.orderId}
            customerId: ${parameters.customerId}
            status: created
  4. Run It

    drift verify -f drift/send.testcases.yaml

    Drift subscribes to drift.examples.order-created, runs the trigger, waits up to five seconds for a message carrying send-order-created-001, then validates it against the message schema and your expected block.

Verifying a Command Your Service Consumes

Your service consumes a command and there is nothing to capture on the broker. You tell Drift how to check the result, using one of two approaches.

Approach

Use it when

Mode

Probe command

The evidence is a database row, an API response, or a file

async-inject

Probe topic

Your test harness can publish its result to a topic

topicasync-inject-capture

Prefer the probe topic when you can. Drift subscribes before it injects, so there is no window in which the result can be missed.

Describe the Operation

The operation has action: receive, because your application is the consumer.

operations:
  receiveInventoryAdjusted:
    action: receive
    channel:
      $ref: "#/channels/inventoryAdjusted"
    messages:
      - $ref: "#/channels/inventoryAdjusted/messages/inventoryAdjusted"

Option A: Check a Side Effect with a Probe Command

sequenceDiagram
    autonumber
    participant D as Drift
    participant B as Kafka
    participant S as Your service
    participant E as State

    D->>B: Publish inventory.adjusted
    B-->>S: Deliver the message
    S->>E: Record the result
    D->>D: Run the probe hook
    D->>E: Probe reads the state
    E-->>D: Probe returns JSON
    D->>D: Compare against expectations
drift-testcase-file: v1
title: "Inventory service inbound commands"

sources:
  - name: async-receive
    path: ../asyncapi/receive.asyncapi.yaml
  - name: setup
    path: ../hooks/reset-state.lua

plugins:
  - name: asyncapi
  - name: kafka
  - name: json

operations:
  ReceiveInventoryAdjusted_Inject:
    target: async-receive:receiveInventoryAdjusted:inventoryAdjusted
    description: "Publish an inventory.adjusted command and check the recorded state"
    parameters:
      correlation-id: receive-inventory-adjusted-001
      payload:
        commandType: inventory.adjusted
        sku: SKU-RED-CHAIR
        quantityDelta: -2
        reason: reservation
    probe:
      executable-type: command
      value: python3
      parameters:
        args:
          - ../hooks/probe-receive.py
          - --correlation-id
          - ${parameters.correlation-id}
      timeout-ms: 3000
    expected:
      status: processed
      correlationId: ${parameters.correlation-id}
      sku: ${parameters.payload.sku}
      quantityDelta: ${parameters.payload.quantityDelta}
      reason: ${parameters.payload.reason}

The expected block matches the JSON the probe writes to standard output, not a message schema. Drift validates the payload it publishes against the AsyncAPI schema, but it cannot schema-validate probe output, because the probe result is not described in the document.

The reset-state.lua source clears state before each operation so a probe cannot report a false pass from an earlier run.

Option B: Capture the Result from a Probe Topic

Set probe-topic and Drift subscribes to that topic before injecting.

sequenceDiagram
    autonumber
    participant D as Drift
    participant B as Kafka
    participant H as Your test harness

    D->>B: Subscribe to the probe topic
    Note over D,B: Before injecting — no race
    D->>B: Publish inventory.adjusted
    opt Trigger configured
        D->>H: Run the trigger hook
    end
    B-->>H: Deliver the injected message
    H->>B: Publish the result to the probe topic
    B-->>D: Deliver the probe topic message
    D->>D: Compare against expectations
operations:
  ReceiveInventoryAdjusted_InjectCapture:
    target: async-receive:receiveInventoryAdjusted:inventoryAdjusted
    description: "Inject inventory.adjusted and capture the harness result"
    parameters:
      probe-topic: drift.examples.inventory-adjusted-probe
      timeout-ms: 5000
      correlation-id: receive-inventory-adjusted-capture-001
      payload:
        commandType: inventory.adjusted
        sku: SKU-RED-CHAIR
        quantityDelta: -2
        reason: reservation
    trigger:
      executable-type: command
      value: python3
      parameters:
        args:
          - ../hooks/trigger-inject-capture.py
          - --correlation-id
          - ${parameters.correlation-id}
          - --probe-topic
          - ${parameters.probe-topic}
      timeout-ms: 8000
    expected:
      headers:
        correlation-id: ${parameters.correlation-id}
      payload:
        status: processed
        sku: ${parameters.payload.sku}
        quantityDelta: ${parameters.payload.quantityDelta}
        reason: ${parameters.payload.reason}

Two constraints:

  • The probe topic must be on the same broker as the injection target.

  • Probe topic messages must be JSON. The topic is not described in your AsyncAPI document, so Drift parses it as application/json and compares against expected without schema validation.

Omit the trigger block when a long-running harness is already subscribed to the input channel. Include it to run a one-shot harness, which needs nothing started in advance.

Using SNS and SQS Instead of Kafka

Every test case above works over Amazon SNS and SQS. Change the transport plugin and the channel address:

plugins:
  - name: asyncapi
  - name: aws-messaging
  - name: json
channels:
  inventoryAdjusted:
    address: arn:aws:sqs:us-east-1:123456789012:inventory-adjusted

SNS and SQS support asynchronous interactions only. Request/reply is not available on those transports. For credentials, region resolution, and running against LocalStack, see AWS Messaging Plugin.

Preparing the Broker

Drift does not create channels. Create the topics or queues in your AsyncAPI document before the run.

Kafka's automatic topic creation fires only when a producer or consumer first touches a topic, which is too late for async-inject-capture - Drift publishes to the input topic before any consumer has subscribed. Create topics in advance.

Where you set a correlation ID rather than letting Drift generate one, give each operation a distinct value. Because Drift publishes real messages to a real broker, a shared value lets a message from one test satisfy another.

See Also

Publication date: