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 |
| Subscribe and capture |
|
Consumes a command |
| Publish, then check the result |
|
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 schemaDescribe the Operation
The operation has
action: send, because your application is the publisher. Declare acorrelationIdlocation 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: createdWrite 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: 3000See Using Triggers and Probes for the full hook reference.
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: createdRun 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 carryingsend-order-created-001, then validates it against the message schema and yourexpectedblock.
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 |
|
Probe topic | Your test harness can publish its result to a topic |
|
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 expectationsdrift-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 expectationsoperations:
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
expectedwithout 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-adjustedSNS 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.