Skip to main content

Consumer Testing Guide

This guide covers how to test your service's API integrations using CVT. Consumer testing validates that your HTTP calls to upstream APIs match their published OpenAPI contracts.

Example Schema

The examples in this guide use the Petstore schema from sdks/shared/openapi.json. Copy it to your project as openapi.json or adjust the paths to reference it directly.

All registerSchema methods accept both file paths and URLs:

// From file
await validator.registerSchema("petstore", "./openapi.json");
// From URL
await validator.registerSchema(
"petstore",
"https://petstore3.swagger.io/api/v3/openapi.json",
);

Overview

Consumer testing answers the question: "Am I calling this API correctly?"

When your service depends on external APIs, you need confidence that:

  • Your requests are properly formatted
  • You handle responses correctly
  • You won't break when upstream APIs change
┌─────────────────────┐     HTTP      ┌─────────────────────┐
│ Your Service │ ────────────► │ Upstream API │
│ (Consumer) │ │ (Producer) │
└─────────────────────┘ └─────────────────────┘
│ │
│ Validate & Register Consumer │ Register Schema (owns it)
▼ ▼
┌───────────────────────────────────────────────────────────┐
│ CVT Server (shared) │
│ Producer's Schema = Source of Truth │
└───────────────────────────────────────────────────────────┘
Who registers the schema?

In production workflows, the producer registers their OpenAPI schema with the shared CVT server — they own the spec and publish it as part of their CI/CD pipeline. Consumers validate their interactions against that registered schema and register themselves as consumers.

The registerSchema calls in the examples below are a convenience for local development and getting started. In a team environment, the producer's pipeline handles schema registration (see the CI/CD Integration Guide).


Quick Start

1. Start the CVT Server

# Using the published Docker image (recommended)
docker run -d -p 9550:9550 -p 9551:9551 ghcr.io/sahina/cvt:latest

# Or using Docker Compose (if you've cloned the repository)
make up

# Or build and run locally
make run-server

2. Write Your First Contract Test

import { ContractValidator } from "@sahina/cvt-sdk";

describe("Petstore Contract", () => {
let validator: ContractValidator;

beforeAll(async () => {
validator = new ContractValidator("localhost:9550");
await validator.registerSchema("petstore", "./openapi.json");
});

afterAll(() => validator.close());

it("GET /pet/{petId} returns valid response", async () => {
const result = await validator.validate(
{ method: "GET", path: "/pet/123" },
{
statusCode: 200,
body: { id: 123, name: "Fluffy", status: "available", photoUrls: [] },
},
);
expect(result.valid).toBe(true);
});
});

Validation Approaches

CVT supports multiple ways to validate your API interactions.

Approach Comparison

ApproachControlNeeds ProducerBest For
ManualFullYesEdge cases, errors
HTTP AdapterMediumYesExisting tests
Mock ClientLowNoCI/CD, unit tests
  • Manual: Call validate() explicitly. Full control for edge cases and error scenarios.
  • HTTP Adapter: Wraps your HTTP client. Automatic validation. Drop-in for existing tests.
  • Mock Client: No real HTTP calls. CVT generates responses from schema. Fast and deterministic.

Choosing an Approach

ScenarioRecommended Approach
CI/CD pipeline without servicesMock Client
Adding validation to existing testsHTTP Adapter
Testing specific error responsesManual Validation
Unit testing consumer logicMock Client
Integration testing with real APIHTTP Adapter

Approach 1: Manual Validation

Build request/response objects and validate them directly:

import { ContractValidator } from "@sahina/cvt-sdk";

const validator = new ContractValidator("localhost:9550");
await validator.registerSchema("petstore", "./openapi.json");

// Validate a specific interaction
const result = await validator.validate(
{
method: "GET",
path: "/pet/123",
headers: { Accept: "application/json" },
},
{
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: { id: 123, name: "Fluffy", status: "available", photoUrls: [] },
},
);

if (!result.valid) {
console.error("Validation errors:", result.errors);
}

validator.close();

Wrap your HTTP client for automatic validation of all requests/responses:

import axios from "axios";
import { ContractValidator } from "@sahina/cvt-sdk";
import { createAxiosAdapter } from "@sahina/cvt-sdk/adapters";

const validator = new ContractValidator("localhost:9550");
await validator.registerSchema("petstore", "./openapi.json");

const api = axios.create({ baseURL: "http://petstore-api" });

// Wrap axios - all traffic is now validated automatically
createAxiosAdapter({
axios: api,
validator,
autoValidate: true,
onValidationFailure: (result) => {
throw new Error(`Contract violation: ${result.errors.join(", ")}`);
},
});

// Use normally - validation happens transparently
const pet = await api.get("/pet/123");

validator.close();

Approach 3: Mock Client

Use the mock adapter for tests without a real API:

import { ContractValidator } from "@sahina/cvt-sdk";
import { createMockAdapter } from "@sahina/cvt-sdk/adapters";

const validator = new ContractValidator("localhost:9550");
await validator.registerSchema("petstore", "./openapi.json");

// Create mock adapter that auto-generates responses from schema
const mock = createMockAdapter({ validator, cache: true });

// Make requests - responses are generated from OpenAPI schema
const response = await mock.fetch("http://mock.petstore/pet/456");
const pet = await response.json();

// Check recorded interactions for consumer registration
const interactions = mock.getInteractions();

validator.close();

Benefits:

  • No real API endpoint needed
  • Responses match schema exactly
  • Interactions captured for auto-registration

Consumer Registration

Register your service as a consumer to enable deployment safety checks.

Prerequisites

Before creating adapters or registering consumers, ensure the schema is registered:

const validator = new ContractValidator("localhost:9550");

// Always register schema first
await validator.registerSchema("petstore", "./openapi.json");

// Now you can create adapters or validate interactions

Register from captured test interactions - endpoints and fields are extracted automatically:

import { ContractValidator } from "@sahina/cvt-sdk";
import { createMockAdapter } from "@sahina/cvt-sdk/adapters";

const validator = new ContractValidator("localhost:9550");
await validator.registerSchema("petstore", "./openapi.json");

// Create mock adapter to capture interactions
const mock = createMockAdapter({ validator, cache: true });

// Run tests using the mock
await mock.fetch("http://mock.petstore/pet/123");
await mock.fetch("http://mock.petstore/pet", { method: "POST", body: "{}" });

// Auto-register consumer from captured interactions
const consumerInfo = await validator.registerConsumerFromInteractions(
mock.getInteractions(),
{
consumerId: "order-service",
consumerVersion: "2.1.0",
environment: "dev",
schemaVersion: "1.0.0",
// schemaId auto-extracted from URL: http://mock.petstore/... -> "petstore"
},
);

console.log(
`Registered ${consumerInfo.consumerId} with ${consumerInfo.usedEndpoints.length} endpoints`,
);
validator.close();

Schema ID Extraction:

  • Auto-extracted from mock URLs: http://mock.petstore/pet/123 extracts petstore
  • Override with explicit schemaId when using non-mock URLs:
const consumerInfo = await validator.registerConsumerFromInteractions(
interactions,
{
consumerId: "order-service",
consumerVersion: "2.1.0",
environment: "dev",
schemaVersion: "1.0.0",
schemaId: "petstore", // Explicit override for non-mock URLs
},
);

Benefits:

  • No manual endpoint specification needed
  • Fields extracted from actual usage
  • Always in sync with test behavior
  • Less maintenance overhead

Manual Registration

For fine-grained control, specify endpoints explicitly:

await validator.registerConsumer({
consumerId: "order-service",
consumerVersion: "2.1.0",
schemaId: "petstore",
schemaVersion: "1.0.0",
environment: "dev",
usedEndpoints: [
{
method: "GET",
path: "/pet/{petId}",
usedFields: ["id", "name", "status", "category.name", "tags"],
},
{
method: "POST",
path: "/pet",
usedFields: ["id"],
},
],
});

Nested Field Paths:

Use dot notation to specify nested fields in usedFields:

  • id - top-level field
  • category.name - nested field
  • category.id - another nested field

This tells CVT:

  • Who you are: order-service v2.1.0
  • What you depend on: petstore v1.0.0
  • What you use: Specific endpoints and fields

Idempotent Registration

Registering the same (consumerId, schemaId, environment) combination will overwrite the previous registration. This allows CI pipelines to re-register on every build without manual cleanup.

Listing Consumers

Query all consumers registered for a schema:

const consumers = await validator.listConsumers("petstore", "dev");
console.log(`${consumers.length} services depend on petstore`);

Deregistering Consumers

Remove a consumer registration when no longer needed:

await validator.deregisterConsumer("order-service", "petstore", "dev");

Deployment Safety (can-i-deploy)

Before deploying a new schema version, use can-i-deploy to check if it will break any registered consumers. For complete CLI usage, SDK examples, output formats, and version pinning details, see the Breaking Changes Guide.


Environment Promotion

For environment promotion workflows (dev -> staging -> prod) and safety checks before promotion, see the Breaking Changes Guide.


Secure Connections

For TLS configuration and API key authentication in SDK clients, see the Configuration Reference.


Testing Scenarios

Valid Interaction

it("returns valid response with all required fields", async () => {
const result = await validator.validate(
{ method: "GET", path: "/pet/123" },
{
statusCode: 200,
body: { id: 123, name: "Fluffy", status: "available", photoUrls: [] },
},
);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});

Invalid Response (Missing Fields)

it("catches missing required fields", async () => {
const result = await validator.validate(
{ method: "GET", path: "/pet/123" },
{
statusCode: 200,
body: { id: 123 }, // missing name and photoUrls (required)
},
);
expect(result.valid).toBe(false);
// Errors are full descriptive messages, use pattern matching
expect(result.errors.some((e) => e.includes("name"))).toBe(true);
});

Error Responses

it("validates 404 error response format", async () => {
const result = await validator.validate(
{ method: "GET", path: "/pet/nonexistent" },
{
statusCode: 404,
body: {},
},
);
// Valid if 404 response matches schema's 404 response definition
expect(result.valid).toBe(true);
});

CI/CD Integration

For complete CI/CD pipeline examples (GitHub Actions, GitLab CI, Jenkins) including contract testing, consumer registration, and deployment safety checks, see the CI/CD Integration Guide.


Corporate Proxy Configuration

For proxy configuration including gRPC traffic, HTTP traffic, SDK-specific settings, and SSL certificate issues, see the Configuration Reference.


Troubleshooting

"Failed to create validator"

Make sure CVT server is running:

make up
# or
docker-compose up -d cvt-server

"Schema not found"

Ensure the schema is registered before validation:

// Register schema first
await validator.registerSchema("my-api", "./openapi.json");

// Then validate
const result = await validator.validate(request, response);

"Path not found"

Check that your path matches the OpenAPI spec:

  • Use actual path values: /pet/123 not /pet/{petId}
  • Ensure the HTTP method matches

Connection refused

Default server address is localhost:9550. Configure if different:

const validator = new ContractValidator("cvt.internal:9550");

Resource cleanup

Always close the validator when done to release gRPC connections:

// In tests
afterAll(() => validator.close());

// In application code
try {
await validator.registerSchema("my-api", "./openapi.json");
// ... use validator
} finally {
validator.close();
}

Next Steps