Contract Testing for AI Microservices API Testing

Contract Testing for AI Microservices: Don't Let Your APIs Break in the Dark

MP
Maya Patel • April 2026

TL;DR

Contract testing ensures your AI microservices don't break integration boundaries. Traditional Pact works for deterministic APIs, but AI services are non-deterministic. You need semantic contracts for probabilistic outputs, schema validation that handles variable responses, version compatibility testing, and consumer-driven contract frameworks adapted for LLMs. We walk through Pact/PactFlow for AI, how to write contracts for hallucination-prone APIs, and real-world examples.

Your recommendation engine service returned a valid JSON response. The schema is correct. The field names are right. But the values are hallucinated. Your consuming service accepted the contract violation, and now your customer is looking at a completely wrong recommendation.

Contract testing, the practice of validating that services meet each other's expectations, is standard in deterministic microservices architectures. But AI microservices break the contract testing paradigm. Traditional tests verify exact outputs. AI services output probabilities, embeddings, and variable text.

The question isn't "did you return the right string?" It's "did you return semantically valid output that respects our contract boundaries?" That's a different kind of testing entirely.

Why Traditional Contract Testing Fails for AI

Traditional Pact contracts look like this:

test('returns user recommendations', () => {
 expect(response).toEqual({
 recommendations: [
 { id: 1, title: 'Product A', score: 0.95 },
 { id: 2, title: 'Product B', score: 0.87 }
 ]
 });
});

This passes or fails. There's no middle ground. But AI APIs generate variable outputs. The same input produces different recommendations every time. Traditional contract testing creates false test failures and forces you to disable contracts entirely.

I've seen teams say "contracts don't work for AI" and drop them completely. Then they integrated breaking changes and didn't realize until production.

Contract testing for AI isn't about exact matches. It's about verifying that boundary contracts, data schemas, output formats, semantic requirements, are respected.

Semantic Contract Testing: The Framework

Semantic contracts validate that outputs meet requirements without requiring exact matches.

Layer 1: Schema Validation

First, verify the response structure is valid. Does it have the required fields? Are types correct? Are values within expected ranges?

const semanticContract = {
 // Consumer: downstream service (e.g., recommendation UI)
 consumer: 'recommendation-ui',

 // Provider: AI service (e.g., recommendation engine)
 provider: 'recommendation-engine',

 // Contract: what the consumer expects from the provider
 contract: {
 request: {
 userId: 'number',
 limit: { type: 'number', min: 1, max: 100 }
 },

 response: {
 // Schema validation: must be present and correctly typed
 recommendations: {
 type: 'array',
 minItems: 1,
 maxItems: 100,
 items: {
 type: 'object',
 required: ['id', 'title', 'score'],
 properties: {
 id: { type: 'number' },
 title: { type: 'string', minLength: 1 },
 score: {
 type: 'number',
 minimum: 0,
 maximum: 1
 }
 }
 }
 },

 // Metadata for debugging
 modelVersion: 'string',
 generatedAt: 'string' // ISO timestamp
 }
 }
};

This validates structure. Every response from your AI service must pass this schema, regardless of what recommendations are generated.

Layer 2: Semantic Validation

Beyond schema, validate semantic requirements. Are the recommendations coherent? Do the scores correlate with quality?

const validateSemanticContract = async (response, context) => {
 const contract = {
 // Recommendations must be diverse
 diversity: {
 validate: (recs) => {
 const embeddings = recs.map(r => embed(r.title));
 const distances = computePairwiseDistances(embeddings);
 const minDistance = Math.min(...distances);
 return minDistance > 0.3; // embedding distance threshold
 },
 error: 'Recommendations too similar'
 },

 // Score distribution must be reasonable
 scoreDistribution: {
 validate: (recs) => {
 const scores = recs.map(r => r.score);
 const variance = calculateVariance(scores);
 return variance > 0.01; // not all same score
 },
 error: 'Scores lack variation'
 },

 // Rankings must be monotonic (higher score = appears earlier)
 monotonicity: {
 validate: (recs) => {
 for (let i = 0; i < recs.length - 1; i++) {
 if (recs[i].score < recs[i + 1].score) {
 return false; // violated monotonicity
 }
 }
 return true;
 },
 error: 'Scores not properly ranked'
 },

 // No hallucinated IDs
 knownIds: {
 validate: async (recs) => {
 const validIds = await getValidProductIds();
 return recs.every(r => validIds.includes(r.id));
 },
 error: 'Contains unknown product IDs'
 }
 };

 const violations = [];

 for (const [rule, validator] of Object.entries(contract)) {
 const passes = await validator.validate(response.recommendations);
 if (!passes) {
 violations.push(validator.error);
 }
 }

 return {
 passes: violations.length === 0,
 violations
 };
};

Layer 3: Version Compatibility Testing

As you update your AI service, contracts ensure backward compatibility. Consumers need to know when breaking changes are coming.

// Pact/PactFlow example for AI services
describe('Recommendation Engine Contract', () => {
 const provider = new Pact({
 consumer: 'recommendation-ui',
 provider: 'recommendation-engine'
 });

 it('returns recommendations matching semantic contract', async () => {
 await provider.addInteraction({
 state: 'user has recommendation history',
 uponReceiving: 'a request for recommendations',
 withRequest: {
 method: 'POST',
 path: '/api/v1/recommend',
 body: { userId: 123, limit: 10 }
 },
 willRespondWith: {
 status: 200,
 // Instead of exact body match, use matcher that validates semantics
 body: {
 recommendations: Matchers.arrayContaining([
 {
 id: Matchers.number(),
 title: Matchers.string(),
 score: Matchers.numberBetween(0,1),
 // Custom matcher for semantic validation
 ...customMatcher(validateSemanticContract)
 }
 ]),
 modelVersion: Matchers.string(),
 generatedAt: Matchers.iso8601DateTime()
 }
 }
 });

 const response = await client.getRecommendations(123,10);
 expect(response).toMatchObject(provider.contract);
 });

 it('maintains backward compatibility across model versions', async () => {
 const v2Schema = { /* new schema */ };
 const v1Schema = { /* old schema */ };

 // Verify v2 output is compatible with v1 consumers
 expect(validateAgainstSchema(v2Response, v1Schema)).toBe(true);
 });
});

Consumer-Driven Contracts for AI APIs

Instead of the provider defining what's correct, consumers define their expectations. This shifts testing upstream.

How Consumer-Driven Contracts Work

Your recommendation UI team defines what they need from the recommendation engine. The engine team verifies they can deliver it. Contract violations are caught before they hit production.

// Consumer (recommendation-ui) defines expectations
const uiContractExpectations = {
 consumer: 'recommendation-ui',
 provider: 'recommendation-engine',
 version: '1.0.0',

 interactions: [
 {
 name: 'Get recommendations for active user',
 given: 'user has browse history',
 uponReceiving: 'request for top 5 recommendations',
 withRequest: {
 method: 'POST',
 path: '/api/recommend',
 body: { userId: 'any-user-id', limit: 5 }
 },
 willRespondWith: {
 status: 200,
 body: {
 recommendations: 'array-with-5-items',
 // Consumer cares about these properties
 items: {
 id: 'must-be-valid-product',
 title: 'must-be-non-empty-string',
 score: 'must-be-between-0-and-1',
 // Consumer doesn't care about exact values
 reasoning: 'any-string-is-ok' // optional explanation
 }
 }
 }
 }
 ]
};

// Provider (recommendation-engine) verifies contract
const verifyContract = async (provider, contract) => {
 const results = [];

 for (const interaction of contract.interactions) {
 const response = await provider.process(interaction.withRequest.body);

 const violation = checkSemanticContract(
 response,
 interaction.willRespondWith,
 interaction.uponReceiving
 );

 results.push({
 interaction: interaction.name,
 passes: !violation,
 violation
 });
 }

 return results;
};

Handling Non-Deterministic Outputs

AI services are non-deterministic. The same input produces different outputs. Your contracts must account for this.

Strategy 1: Repeated Validation

Run the same interaction multiple times. All responses must pass semantic validation.

const validateNonDeterministicContract = async (provider, contract) => {
 const trials = 10;
 const failures = [];

 for (let i = 0; i < trials; i++) {
 const response = await provider.process(contract.request);

 const semanticValidation = await validateSemanticContract(
 response,
 contract
 );

 if (!semanticValidation.passes) {
 failures.push({
 trial: i,
 violations: semanticValidation.violations
 });
 }
 }

 if (failures.length > 0) {
 console.error(`Contract failed ${failures.length}/${trials} times`);
 return {
 passes: false,
 failureRate: failures.length / trials,
 failures
 };
 }

 return { passes: true };
};

Strategy 2: Property-Based Testing

Use property-based testing to validate that contracts hold across a range of inputs, not just fixed examples.

const propertyBasedContractTest = async (provider) => {
 const properties = [
 {
 name: 'Recommendations always have valid IDs',
 property: (response) => {
 return response.recommendations.every(r =>
 validProductIds.includes(r.id)
 );
 }
 },
 {
 name: 'Scores are monotonically decreasing',
 property: (response) => {
 const scores = response.recommendations.map(r => r.score);
 for (let i = 0; i < scores.length - 1; i++) {
 if (scores[i] < scores[i + 1]) return false;
 }
 return true;
 }
 },
 {
 name: 'Scores are between 0 and 1',
 property: (response) => {
 return response.recommendations.every(r =>
 r.score >= 0 && r.score <= 1
 );
 }
 }
 ];

 const userIds = generateUserIds(100); // Generate test inputs

 for (const userId of userIds) {
 const response = await provider.recommend(userId);

 for (const prop of properties) {
 if (!prop.property(response)) {
 console.error(`Property failed: ${prop.name} for userId ${userId}`);
 return false;
 }
 }
 }

 return true;
};

Detecting Contract Violations in Production

Contracts should be validated at the integration boundary, not just in tests. Catch violations immediately when they occur.

const contractEnforcingMiddleware = (contractDefinition) => {
 return async (req, res, next) => {
 const originalSend = res.send;

 res.send = function(data) {
 const violation = checkSemanticContract(data, contractDefinition);

 if (!violation.passes) {
 // Log contract violation
 logger.error('Contract violation detected', {
 path: req.path,
 consumer: contractDefinition.consumer,
 provider: contractDefinition.provider,
 violations: violation.violations,
 response: data
 });

 // Metrics and alerting
 metrics.contractViolations.inc();

 // In strict mode, reject the response
 if (process.env.CONTRACT_MODE === 'strict') {
 return res.status(502).json({
 error: 'Service contract violation',
 details: violation.violations
 });
 }
 }

 return originalSend.call(this, data);
 };

 next();
 };
};

Real-World Contract Testing Workflow

mature teams implement contract testing for AI microservices:

1. Consumer Defines Contract
The recommendation UI team publishes their contract requirements. They specify required fields, value ranges, and semantic constraints.

2. Provider Implements Against Contract
The recommendation engine team verifies their implementation passes all consumer expectations before changes merge to main.

3. Contract Versioning
When the recommendation engine wants to add fields, it does so without removing old ones. Backward compatibility is verified.

4. Production Validation
Contract-enforcing middleware validates responses at integration boundaries. Contract violations trigger alerts immediately.

5. Breaking Changes Handled Explicitly
If a breaking change is necessary, both teams agree, deprecation periods are announced, and migration is tracked.

Contract Testing Checklist for AI Services

Before deploying any AI microservice update:

Schema Layer
✓ Response has all required fields
✓ Field types are correct
✓ Numeric values within expected ranges
✓ Array bounds respected (min/max items)

Semantic Layer
✓ Outputs pass semantic validation
✓ No hallucinations in deterministic fields
✓ Probabilistic outputs are coherent
✓ Constraints are respected

Compatibility Layer
✓ Backward compatible with v-1 consumers
✓ New fields don't break old clients
✓ Model version tracked in response
✓ Versioning strategy documented

Production Layer
✓ Contract validation enabled at boundaries
✓ Violations logged and alerted
✓ Monitoring dashboard active
✓ Rollback procedure in place

The Contract Testing Reality

Traditional contract testing breaks for AI. But semantic contract testing, validating that outputs meet structural, semantic, and compatibility requirements, gives you the benefits of contract testing without the brittleness of exact-match validation.

Your AI microservices need contracts. Not to guarantee exact outputs, but to catch integration failures before they cascade through your system.

Contract Testing for AI Microservices

alt.qa's contract testing framework validates semantic contracts, handles non-deterministic outputs, and prevents breaking changes in AI service boundaries.

Explore Contract Testing Tools
Maya Patel specializes in AI microservices architecture at alt.qa. She's designed contract testing frameworks for services handling recommendation engines, content moderation, and language generation at scale. Maya is passionate about catching integration failures before production.