Resource library

QA How-To

How to Test Event Driven APIs With AsyncAPI (2026)

Learn to test event driven APIs with AsyncAPI using schema validation, executable Node.js contract tests, negative cases, compatibility checks, and CI.

22 min read | 2,900 words

TL;DR

Define the channel, operation, headers, and payload in AsyncAPI 3.0. Validate the document with the AsyncAPI CLI, compile the referenced JSON Schema with Ajv, run positive and negative Node tests, and gate specification changes in CI.

Key Takeaways

  • Validate the AsyncAPI document before testing payloads so specification defects fail separately from event defects.
  • Treat the message schema as executable test data and compile it with a JSON Schema validator.
  • Test valid, missing, malformed, and additional properties instead of relying on one happy-path event.
  • Check headers, correlation IDs, and channel bindings as contracts, not incidental broker details.
  • Compare the base and proposed specifications in CI to catch breaking changes before deployment.
  • Keep broker integration tests focused because fast contract tests already cover the schema boundary.

To test event driven APIs with AsyncAPI, turn the API description into an executable contract. First validate the AsyncAPI document itself. Then resolve its message payload and headers, compile those JSON Schemas, and exercise representative valid and invalid events. This separates a malformed specification from a producer or consumer that violates a correct specification.

This tutorial builds a small but realistic order-events contract. You will test an order.created Kafka event without starting Kafka, then add a compatibility and CI gate. For wider architectural context, read the event-driven API testing complete guide.

The central idea is simple: AsyncAPI describes what may travel through a channel, while executable tests prove that concrete messages obey that description. Broker integration tests remain useful for delivery, authentication, partitions, and retries, but they should not carry every payload edge case.

What You Will Build

You will create a repository that provides:

  • An AsyncAPI 3.0.0 document for an order service sending order.created events to Kafka.
  • A CLI check that rejects invalid AsyncAPI syntax and structure.
  • Node.js contract tests that resolve the message reference and validate payloads and headers with Ajv.
  • Negative tests for missing fields, invalid formats, unknown properties, and header defects.
  • A semantic compatibility test that detects removed required fields and newly required fields.
  • A GitHub Actions workflow that runs all checks on every pull request.

The example deliberately tests the contract without a broker. That makes the suite deterministic and fast enough for every commit. Once it passes, add a thin broker test for serialization and delivery. If your system relies heavily on retries, pair this tutorial with testing dead-letter queue retries.

Prerequisites

Use these exact tutorial versions:

  • Node.js 24.x and npm 11.x.
  • AsyncAPI specification 3.0.0.
  • @asyncapi/cli 6.0.2.
  • ajv 8.17.1 and ajv-formats 3.0.1.
  • yaml 2.8.1.

Check your runtime, create a directory, and install the pinned packages:

node --version
npm --version
mkdir asyncapi-order-contract && cd asyncapi-order-contract
npm init -y
npm install --save-dev @asyncapi/cli@6.0.2 ajv@8.17.1 ajv-formats@3.0.1 yaml@2.8.1
mkdir -p contracts test scripts .github/workflows

Edit package.json so its scripts and module type are exactly these values. Keep the fields generated by npm init as they are.

{
  "type": "module",
  "scripts": {
    "validate:asyncapi": "asyncapi validate contracts/order-events.yaml --fail-severity=error",
    "test:contract": "node --test test/*.test.js",
    "test": "npm run validate:asyncapi && npm run test:contract"
  }
}

Verify: run npm exec asyncapi -- --version. Confirm the output starts with @asyncapi/cli/6.0.2. Run npm ls ajv ajv-formats yaml and confirm npm reports the pinned versions without invalid or missing.

Step 1: Model the Event Contract in AsyncAPI

Create contracts/order-events.yaml. AsyncAPI 3 separates channels from operations. The channel declares the Kafka address and its messages. The operation says the application sends a message to that channel.

asyncapi: 3.0.0
info:
  title: Order Events API
  version: 1.0.0
  description: Events emitted by the order service.
servers:
  production:
    host: kafka.example.com:9092
    protocol: kafka-secure
channels:
  orderCreated:
    address: orders.created.v1
    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
      title: Order created event
      correlationId:
        location: '$message.header#/correlationId'
      headers:
        type: object
        additionalProperties: false
        required: [correlationId, eventType]
        properties:
          correlationId:
            type: string
            format: uuid
          eventType:
            const: order.created
      payload:
        type: object
        additionalProperties: false
        required: [eventId, occurredAt, orderId, customerId, total]
        properties:
          eventId:
            type: string
            format: uuid
          occurredAt:
            type: string
            format: date-time
          orderId:
            type: string
            pattern: '^ord_[A-Za-z0-9]+
#39; customerId: type: string minLength: 1 total: type: object additionalProperties: false required: [amount, currency] properties: amount: type: integer minimum: 0 currency: type: string pattern: '^[A-Z]{3}
#39;

Several details are testable. additionalProperties: false blocks accidental data leakage and misspelled fields. The amount is an integer in minor currency units, so 2599 means 25.99 in the stated currency. A UUID correlation header connects logs across services. The versioned topic address gives incompatible event families somewhere explicit to live.

Verify: run npm run validate:asyncapi. The command must exit with code 0. Warnings about optional descriptions may appear, but errors must not. If it reports a line and column, fix the document before writing payload tests.

Step 2: Test Event Driven APIs With AsyncAPI Document Validation

A document validation check answers, "Is this a legal AsyncAPI document?" It catches a missing required object, an invalid operation action, a broken local reference, or a property placed at the wrong level. It does not prove that your application emits a compliant instance. Keep this check separate because its failure belongs to the contract owner.

Run the CLI directly when diagnosing a change:

npx asyncapi validate contracts/order-events.yaml \
  --fail-severity=error \
  --diagnostics-format=stylish

You can also save machine-readable diagnostics for CI tooling:

npx asyncapi validate contracts/order-events.yaml \
  --fail-severity=error \
  --diagnostics-format=json \
  --save-output=asyncapi-diagnostics.json

Do not confuse validation with governance. The specification allows many optional fields that your organization might require. For example, you may demand an owner extension, descriptions on every message, or a version suffix on Kafka topics. Those are lint rules layered above specification validity. Start with the official validator, then add governance only when a rule has an owner and a clear remediation.

Check Defect caught Runs without broker Best failure owner
AsyncAPI CLI validation Illegal document or broken reference Yes Contract author
JSON Schema instance test Invalid payload or headers Yes Producer or consumer developer
Compatibility comparison Breaking schema evolution Yes API reviewer
Kafka integration test Topic, auth, serialization, or delivery defect No Platform and service teams

Verify: temporarily change action: send to action: publish, which is not an AsyncAPI 3 operation action. Run the validator and confirm a nonzero exit. Restore action: send, rerun it, and confirm success. This controlled mutation proves the command can fail instead of merely printing a green-looking message.

Step 3: Load and Resolve the Message Schema

Create scripts/load-contract.js. The tutorial uses a small explicit local-reference resolver rather than hiding the behavior behind a framework. It supports the two reference paths used by this contract and throws on a missing segment. That failure is much clearer than an eventual undefined error.

import { readFile } from 'node:fs/promises';
import YAML from 'yaml';

export async function loadContract(
  file = new URL('../contracts/order-events.yaml', import.meta.url)
) {
  return YAML.parse(await readFile(file, 'utf8'));
}

export function resolveLocalRef(document, ref) {
  if (!ref.startsWith('#/')) {
    throw new Error(`Only local references are supported: ${ref}`);
  }

  return ref
    .slice(2)
    .split('/')
    .map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'))
    .reduce((current, part) => {
      if (current === undefined || !(part in current)) {
        throw new Error(`Unresolved reference: ${ref}`);
      }
      return current[part];
    }, document);
}

export async function loadOrderCreatedMessage() {
  const document = await loadContract();
  const channelEntry = document.channels.orderCreated.messages.OrderCreated;
  return resolveLocalRef(document, channelEntry.$ref);
}

JSON Pointer escapes / as ~1 and ~ as ~0, which the resolver decodes. External files and remote URLs need a fuller bundling strategy, but keeping this example local avoids network-dependent tests. For a larger contract library, bundle documents before compiling schemas and test that bundle as the release artifact.

Verify: execute this one-line module command:

node --input-type=module -e "import('./scripts/load-contract.js').then(async m => console.log((await m.loadOrderCreatedMessage()).name))"

The terminal must print OrderCreated. An unresolved-reference exception means the channel reference and component path have drifted.

Step 4: Compile Payload and Header Validators

Create scripts/validators.js. Ajv compiles JSON Schema into validation functions. allErrors: true reports every useful defect in a sample, while strict: false tolerates AsyncAPI-adjacent annotations that are irrelevant to instance validation. Formats are registered explicitly because modern Ajv keeps them in ajv-formats.

import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { loadOrderCreatedMessage } from './load-contract.js';

export async function createMessageValidators() {
  const message = await loadOrderCreatedMessage();
  const ajv = new Ajv({ allErrors: true, strict: false });
  addFormats(ajv);

  return {
    validatePayload: ajv.compile(message.payload),
    validateHeaders: ajv.compile(message.headers)
  };
}

export function validationText(validate) {
  return (validate.errors ?? [])
    .map((error) => `${error.instancePath || '/'} ${error.message}`)
    .join('; ');
}

Compile once per test module rather than once per test case. Compilation is setup work; validation is the behavior under test. Preserve validate.errors immediately after calling a validator because the next call replaces that state. In production helpers, return a copied error array if callers need to retain it.

Verify: compile both functions from the shell:

node --input-type=module -e "import('./scripts/validators.js').then(async m => { const v = await m.createMessageValidators(); console.log(typeof v.validatePayload, typeof v.validateHeaders); })"

Expect function function. A format registration problem usually means ajv-formats is missing or imported incorrectly.

Step 5: Test Valid Payloads and Headers

Create test/order-created.test.js. The fixture expresses the smallest representative valid event. Fixed UUIDs and timestamps make failures reproducible, and an integer amount avoids floating-point ambiguity.

import test from 'node:test';
import assert from 'node:assert/strict';
import { createMessageValidators, validationText } from '../scripts/validators.js';

const { validatePayload, validateHeaders } = await createMessageValidators();

const validPayload = {
  eventId: '2f1c5d20-f8d9-4e3d-a79a-41a8168d7c10',
  occurredAt: '2026-08-06T10:30:00Z',
  orderId: 'ord_A19X',
  customerId: 'cus_482',
  total: { amount: 2599, currency: 'USD' }
};

const validHeaders = {
  correlationId: '6d67e090-e7a3-49c9-a1bb-183245c900d3',
  eventType: 'order.created'
};

test('accepts a contract-compliant OrderCreated message', () => {
  assert.equal(
    validatePayload(validPayload),
    true,
    validationText(validatePayload)
  );
  assert.equal(
    validateHeaders(validHeaders),
    true,
    validationText(validateHeaders)
  );
});

A positive contract test is necessary but weak on its own. It proves that at least one instance fits. It does not prove that constraints reject bad instances, nor that your actual producer constructs this shape. Later, call the same validator against a captured producer object before serialization or against a decoded consumer fixture.

Keep secrets and real personal data out of fixtures. The identifiers here are synthetic, stable, and structurally realistic. If test data design is a broader concern, use the API test data management guide.

Verify: run npm run test:contract. Node should report one passing test and zero failures. If the assertion fails, its message prints the schema paths and violated keywords.

Step 6: Add Negative Event Payload Tests

Append these cases to test/order-created.test.js. Each mutation targets one distinct rule, so a failure tells you which guarantee disappeared. Avoid a single garbage object that violates ten constraints because it provides poor regression localization.

const invalidCases = [
  {
    name: 'missing orderId',
    mutate: ({ orderId, ...payload }) => payload,
    expectedKeyword: 'required'
  },
  {
    name: 'negative minor-unit amount',
    mutate: (payload) => ({
      ...payload,
      total: { ...payload.total, amount: -1 }
    }),
    expectedKeyword: 'minimum'
  },
  {
    name: 'lowercase currency code',
    mutate: (payload) => ({
      ...payload,
      total: { ...payload.total, currency: 'usd' }
    }),
    expectedKeyword: 'pattern'
  },
  {
    name: 'unknown top-level property',
    mutate: (payload) => ({ ...payload, cardNumber: 'do-not-emit' }),
    expectedKeyword: 'additionalProperties'
  },
  {
    name: 'invalid event timestamp',
    mutate: (payload) => ({ ...payload, occurredAt: 'yesterday' }),
    expectedKeyword: 'format'
  }
];

for (const { name, mutate, expectedKeyword } of invalidCases) {
  test(`rejects ${name}`, () => {
    assert.equal(validatePayload(mutate(validPayload)), false);
    assert.ok(
      validatePayload.errors.some((error) => error.keyword === expectedKeyword),
      validationText(validatePayload)
    );
  });
}

test('rejects malformed message headers', () => {
  const headers = { ...validHeaders, correlationId: 'trace-123' };
  assert.equal(validateHeaders(headers), false);
  assert.ok(validateHeaders.errors.some((error) => error.keyword === 'format'));
});

These tests protect business meaning, not merely JSON types. A nonnegative integer represents money consistently. Uppercase ISO-style currency codes reduce consumer normalization. Blocking unknown properties can catch a sensitive field before it becomes part of a durable event log. Test headers independently because transports often separate them from the body and application code can corrupt either side.

For delivery behaviors such as duplicates and ordering, schema validation is insufficient. Add scenario tests using the techniques in validate webhook event ordering and duplicates, adapting the sequence assertions to your broker.

Verify: rerun npm run test:contract. Expect seven passing tests: one valid message, five invalid payloads, and one invalid-header case. Change minimum: 0 to minimum: -1 temporarily and confirm the negative-amount test fails, then restore the contract.

Step 7: Detect Breaking AsyncAPI Changes

Schema validity does not mean compatibility. Removing a previously required payload property can break consumers that read it. Adding a new required property can break existing producers. Create contracts/order-events.base.yaml as a copy of the approved contract, then create test/compatibility.test.js.

import test from 'node:test';
import assert from 'node:assert/strict';
import { loadContract, resolveLocalRef } from '../scripts/load-contract.js';

function payloadFor(document) {
  const entry = document.channels.orderCreated.messages.OrderCreated;
  return resolveLocalRef(document, entry.$ref).payload;
}

test('proposed payload preserves required-field compatibility', async () => {
  const base = payloadFor(await loadContract(
    new URL('../contracts/order-events.base.yaml', import.meta.url)
  ));
  const proposed = payloadFor(await loadContract());

  const removedProperties = Object.keys(base.properties)
    .filter((name) => !(name in proposed.properties));
  const newlyRequired = proposed.required
    .filter((name) => !base.required.includes(name));

  assert.deepEqual(removedProperties, [],
    `Removed payload properties: ${removedProperties.join(', ')}`);
  assert.deepEqual(newlyRequired, [],
    `New required properties: ${newlyRequired.join(', ')}`);
});

This is a focused policy, not a universal compatibility algorithm. It catches two high-value changes for this object schema. Production governance may also forbid type changes, tighter ranges, narrowed enums, changed formats, removed channels, or altered security requirements. Compatibility direction depends on who owns serialization and which old/new producer-consumer combinations must coexist.

Prefer additive optional fields for compatible evolution. When semantics truly break, create a new message or channel version and operate both during migration. The same principle appears in testing API versioning, even though request-response deployment mechanics differ.

Verify: run cp contracts/order-events.yaml contracts/order-events.base.yaml, then npm test. Next, add region to the proposed payload properties and to its required list. Confirm the compatibility test reports New required properties: region. Undo that mutation.

Step 8: Run AsyncAPI Contract Tests in CI

Create .github/workflows/asyncapi-contract.yml. Pin the Node major version, use npm ci for the lockfile, validate the document, and run the complete test suite. The workflow uploads diagnostics only when validation fails, keeping successful runs uncluttered.

name: AsyncAPI contract

on:
  pull_request:
    paths:
      - 'contracts/**'
      - 'scripts/**'
      - 'test/**'
      - 'package.json'
      - 'package-lock.json'
      - '.github/workflows/asyncapi-contract.yml'

jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - name: Validate AsyncAPI document
        run: npm run validate:asyncapi
      - name: Run event contract tests
        run: npm run test:contract

Commit package-lock.json; without it, npm ci cannot reproduce the local dependency graph. Protect the default branch with this job as a required check. Review changes to contracts/order-events.base.yaml carefully because updating the baseline in the same pull request can conceal a break. A stronger setup loads the base contract from the merge base or a published artifact.

Do not put a real Kafka credential into this workflow. Contract tests need files only. When you later add broker tests, use short-lived credentials, a disposable topic namespace, bounded polling, and cleanup. The secrets management in CI for tests guide covers that boundary.

Verify: run npm test locally and confirm all eight tests pass. Push a branch and open a pull request. The Checks view should show the AsyncAPI contract / contract job with separate validation and test steps.

Test Event Driven APIs With AsyncAPI Beyond the Schema

The contract suite covers document structure, payload rules, headers, and a compatibility policy. It does not establish that Kafka accepted the record, the producer selected the right partition key, a consumer committed its offset, or a retry reached a dead-letter topic. Those facts require an actual transport and observable service behavior.

Use a layered strategy:

  1. Run document and instance tests on every commit. They should own the broad matrix of field-level cases.
  2. Run a smaller component test against the producer function and validate the object it sends. This catches mapping defects between domain data and the event.
  3. Run broker integration tests in a disposable environment for topic configuration, encoding, headers, keys, authentication, and consumption.
  4. Run end-to-end tests only for critical business journeys, such as an accepted order eventually producing a fulfillment command.
  5. Observe production through schema-aware telemetry and quarantine malformed events instead of allowing endless consumer retries.

A Kafka contract test should assert the decoded record rather than sleep for a fixed number of seconds. Generate a unique correlation ID, publish, poll until a bounded deadline, filter unrelated records, validate the matched headers and value, and commit or clean up deterministically. For consumer evolution, replay representative historical events through the new consumer build. That exposes assumptions that never reached the formal schema.

AsyncAPI is the source of truth for interface intent, but evidence comes from several layers. Keep each assertion at the cheapest layer capable of detecting the defect.

Best Practices

  • Store the AsyncAPI document beside the code that changes it, and require API review for contract diffs.
  • Give every event a stable name, explicit schema, correlation identifier, and documented ownership.
  • Represent money, timestamps, identifiers, and enums consistently across messages.
  • Reject unknown properties when silent producer drift or sensitive-field leakage is a greater risk than forward compatibility.
  • Use examples as documentation, but use generated or hand-built boundary fixtures for tests. One example is not coverage.
  • Validate at producer and consumer boundaries. A producer check prevents bad publication; a consumer check protects against historical or third-party events.
  • Version intentionally. Do not edit an immutable historical event meaning and hope every consumer deploys simultaneously.
  • Keep compatibility rules in code and make the protected direction explicit in the test name.
  • Distinguish contract failures from transport failures in reports so the right team owns remediation.
  • Test idempotency, duplicates, ordering, retries, and dead-letter behavior separately because JSON Schema cannot express temporal guarantees.

Interview Questions and Answers

Q: What does AsyncAPI validation prove?

It proves that the description follows the AsyncAPI specification, including legal structure and resolvable references. It does not prove that a runtime event instance matches the payload schema or that a broker delivers it. Add instance validation and transport tests for those risks.

Q: Why test payloads without Kafka?

Most payload defects are deterministic schema violations and do not need network infrastructure. Broker-free tests run faster, fail more clearly, and allow a broad negative matrix. Keep a smaller Kafka suite for serialization, headers, partitioning, authentication, and delivery.

Q: How do you test backward compatibility?

Compare the approved and proposed contracts using rules aligned with consumer expectations. Common checks reject removed properties, newly required fields, narrowed enums, and incompatible type or format changes. When a break is necessary, introduce a versioned message or channel and migrate consumers deliberately.

Q: What should be validated in event headers?

Validate correlation or trace identifiers, event type, schema version, tenant context when applicable, and content type. Do not put secrets or unnecessary personal data in headers because broker tooling and logs often expose them. Header validation belongs beside payload validation even when the transport stores them separately.

Q: How do you test duplicate delivery?

Publish the same logical event more than once with the same stable event ID, then assert that the consumer side effect occurs once. Inspect the idempotency record or resulting business state, not only consumer logs. Also test a different event ID carrying similar data so deduplication is not overly broad.

Q: When should additionalProperties be false?

Use it when undeclared fields indicate drift, typos, or a data exposure risk. Avoid applying it reflexively if consumers must tolerate producer additions, because strict rejection can undermine forward compatibility. Decide at each object boundary and encode that policy in compatibility tests.

Troubleshooting

Problem: asyncapi validate reports an unresolved $ref. -> Read the complete JSON Pointer from left to right and confirm every case-sensitive segment exists. A message key under channels may refer to components/messages, but renaming only one side breaks the document. Run validation before instance tests so the reference defect stays obvious.

Problem: Ajv says unknown format "uuid" ignored. -> Install the pinned ajv-formats package and call addFormats(ajv) before compiling the schema. Do not replace UUID validation with a loose handwritten regex unless your contract intentionally accepts a nonstandard identifier.

Problem: A negative test fails but validate.errors is empty or unrelated. -> Read errors immediately after the corresponding validator call. Ajv stores errors on the compiled function and replaces them on the next invocation. Avoid calling the validator twice before inspecting its error array.

Problem: The CI job cannot run npm ci. -> Commit the package-lock.json created by the pinned installation and keep it synchronized with package.json. Reproduce the job locally with a clean install when lockfile conflicts occur.

Problem: The compatibility test passes after an obviously breaking edit. -> Make sure the base file is an unchanged approved contract and the proposed file is the working contract. Expanding the focused checker may be necessary for enum narrowing, type changes, constraints, operations, or channels that its current rules do not inspect.

Problem: Broker integration tests time out intermittently. -> Replace fixed sleeps with correlation-based polling and a bounded deadline. Use unique topic or consumer-group identifiers, filter unrelated records, emit useful diagnostics on timeout, and clean up resources even after assertion failures.

Where To Go Next

You now have an executable answer to how to test event driven APIs with AsyncAPI: validate the description, compile message schemas, challenge them with positive and negative instances, enforce evolution rules, and run the suite in CI. This catches contract drift before a producer publishes a durable incompatible event.

Next, add one disposable-broker test that publishes the valid fixture and validates the decoded record. Then cover the operational behavior most relevant to your architecture:

Keep the fast schema matrix local and in every pull request. Let the smaller broker suite prove transport behavior. That division produces clearer failures, lower infrastructure cost, and a contract reviewers can understand before code reaches an environment.

Interview Questions and Answers

How would you test an event-driven API described by AsyncAPI?

I would first validate the AsyncAPI document and all references. Next I would compile the message payload and header schemas, then run positive, boundary, and focused negative instance tests. I would add compatibility checks against the approved contract and a small broker integration suite for serialization, routing, and delivery.

What is the difference between AsyncAPI document validation and message validation?

Document validation checks that the API description conforms to the AsyncAPI specification. Message validation checks that a concrete payload and its headers conform to schemas inside that valid description. Both can pass while broker configuration is still wrong, which is why transport integration tests form another layer.

Why should contract tests run without a message broker?

Field-level contract behavior is deterministic and does not require network infrastructure. Broker-free tests are faster, easier to reproduce, and better suited to a large negative matrix. A focused broker suite should cover only risks introduced by the transport and deployment configuration.

How would you verify event schema compatibility?

I would define the producer-consumer versions that must coexist, then compare the base and proposed schemas in that direction. The gate would inspect removed properties, new required fields, type changes, narrowed enums, tighter constraints, and removed messages or channels. A justified breaking change would use explicit versioning and a migration window.

How do you test idempotency for an event consumer?

Publish the same logical event twice with the same event ID and wait for processing through correlation-aware polling. Assert the durable business side effect occurs once and that deduplication state is recorded as designed. Then publish a similar event with a new ID to prove the guard does not suppress legitimate work.

What contract checks cannot JSON Schema express?

JSON Schema cannot prove delivery order, at-least-once behavior, retry timing, partition selection, offset commits, or cross-message business invariants. Those require sequence, component, or broker integration tests. It also cannot prove that the producer actually publishes the schema-compliant object unless the runtime boundary is exercised.

How would you make an event-driven test suite resistant to flakiness?

I would keep the broad contract matrix broker-free, use unique correlation IDs and consumer groups for integration tests, and poll until a bounded deadline instead of sleeping. Tests would filter unrelated records, expose received messages on timeout, and clean up topics or subscriptions deterministically.

Frequently Asked Questions

Can AsyncAPI be used for testing Kafka events?

Yes. Describe Kafka channels, messages, headers, and payloads in an AsyncAPI document, validate the document, and compile its message schemas for instance tests. Add a smaller real-Kafka test for topic configuration, serialization, partition keys, and delivery semantics.

Does AsyncAPI replace event broker integration tests?

No. AsyncAPI contract tests cover interface structure and message instances without infrastructure. Broker tests are still needed for authentication, encoding, routing, delivery, offsets, retries, and other runtime behavior.

How do I validate an AsyncAPI file from the command line?

Install `@asyncapi/cli` and run `asyncapi validate contracts/order-events.yaml --fail-severity=error`. A zero exit code means the document passed the configured severity gate, not that every runtime event is valid.

How do I test an event payload against AsyncAPI?

Resolve the message referenced by the channel, take its payload JSON Schema, and compile it with a validator such as Ajv. Assert valid fixtures pass and focused invalid fixtures fail for the expected schema keyword.

What negative cases should event contract tests include?

Cover missing required fields, wrong types, invalid formats, range boundaries, enum or pattern violations, and unknown properties according to your schema policy. Test headers separately and add temporal tests for duplicates, ordering, and retries.

How can CI detect breaking AsyncAPI changes?

Compare the approved base contract with the proposed contract and fail on changes incompatible with supported producer-consumer combinations. Typical rules include removed fields, new required fields, narrowed enums, changed types, and removed channels or messages.

Should AsyncAPI schemas allow additional properties?

It depends on the evolution policy. Disallowing them catches typos, unreviewed drift, and possible data leakage, while allowing them lets older consumers tolerate additive producer fields. Make the choice explicitly for each object and test it.

Related Guides