api-contract-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

API Contract Testing

API契约测试

Overview

概述

Contract testing verifies that APIs honor their contracts between consumers and providers. It ensures that service changes don't break dependent consumers without requiring full integration tests. Contract tests validate request/response formats, data types, and API behavior independently.
契约测试用于验证消费者与提供者之间的API是否遵守约定。它无需完整的集成测试,就能确保服务变更不会破坏依赖它的消费者。契约测试可独立验证请求/响应格式、数据类型以及API行为。

When to Use

适用场景

  • Testing microservices communication
  • Preventing breaking API changes
  • Validating API versioning
  • Testing consumer-provider contracts
  • Ensuring backward compatibility
  • Validating OpenAPI/Swagger specifications
  • Testing third-party API integrations
  • Catching contract violations in CI
  • 测试微服务间的通信
  • 防止API破坏性变更
  • 验证API版本控制
  • 测试消费者-提供者契约
  • 确保向后兼容性
  • 验证OpenAPI/Swagger规范
  • 测试第三方API集成
  • 在CI流程中捕获契约违规问题

Key Concepts

核心概念

  • Consumer: Service that calls an API
  • Provider: Service that exposes the API
  • Contract: Agreement on API request/response format
  • Pact: Consumer-defined expectations
  • Schema: Structure definition (OpenAPI, JSON Schema)
  • Stub: Generated mock from contract
  • Broker: Central repository for contracts
  • 消费者(Consumer):调用API的服务
  • 提供者(Provider):暴露API的服务
  • 契约(Contract):关于API请求/响应格式的约定
  • Pact:由消费者定义的预期
  • Schema:结构定义(如OpenAPI、JSON Schema)
  • Stub:基于契约生成的模拟服务
  • Broker:契约的中央存储库

Instructions

操作指南

1. Pact for Consumer-Driven Contracts

1. 基于Pact的消费者驱动契约测试

Consumer Test (Jest/Pact)

消费者测试(Jest/Pact)

typescript
// tests/pact/user-service.pact.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { UserService } from '../../src/services/UserService';

const { like, eachLike, iso8601DateTimeWithMillis } = MatchersV3;

const provider = new PactV3({
  consumer: 'OrderService',
  provider: 'UserService',
  port: 1234,
  dir: './pacts',
});

describe('User Service Contract', () => {
  const userService = new UserService('http://localhost:1234');

  describe('GET /users/:id', () => {
    test('returns user when found', async () => {
      await provider
        .given('user with ID 123 exists')
        .uponReceiving('a request for user 123')
        .withRequest({
          method: 'GET',
          path: '/users/123',
          headers: {
            Authorization: like('Bearer token'),
          },
        })
        .willRespondWith({
          status: 200,
          headers: {
            'Content-Type': 'application/json',
          },
          body: {
            id: like('123'),
            email: like('user@example.com'),
            name: like('John Doe'),
            age: like(30),
            createdAt: iso8601DateTimeWithMillis('2024-01-01T00:00:00.000Z'),
            role: like('user'),
          },
        })
        .executeTest(async (mockServer) => {
          const user = await userService.getUser('123');

          expect(user.id).toBe('123');
          expect(user.email).toBeDefined();
          expect(user.name).toBeDefined();
        });
    });

    test('returns 404 when user not found', async () => {
      await provider
        .given('user with ID 999 does not exist')
        .uponReceiving('a request for non-existent user')
        .withRequest({
          method: 'GET',
          path: '/users/999',
        })
        .willRespondWith({
          status: 404,
          headers: {
            'Content-Type': 'application/json',
          },
          body: {
            error: like('User not found'),
            code: like('USER_NOT_FOUND'),
          },
        })
        .executeTest(async (mockServer) => {
          await expect(userService.getUser('999')).rejects.toThrow(
            'User not found'
          );
        });
    });
  });

  describe('POST /users', () => {
    test('creates new user', async () => {
      await provider
        .given('user does not exist')
        .uponReceiving('a request to create user')
        .withRequest({
          method: 'POST',
          path: '/users',
          headers: {
            'Content-Type': 'application/json',
          },
          body: {
            email: like('newuser@example.com'),
            name: like('New User'),
            age: like(25),
          },
        })
        .willRespondWith({
          status: 201,
          headers: {
            'Content-Type': 'application/json',
          },
          body: {
            id: like('new-123'),
            email: like('newuser@example.com'),
            name: like('New User'),
            age: like(25),
            createdAt: iso8601DateTimeWithMillis(),
            role: 'user',
          },
        })
        .executeTest(async (mockServer) => {
          const user = await userService.createUser({
            email: 'newuser@example.com',
            name: 'New User',
            age: 25,
          });

          expect(user.id).toBeDefined();
          expect(user.email).toBe('newuser@example.com');
        });
    });
  });

  describe('GET /users/:id/orders', () => {
    test('returns user orders', async () => {
      await provider
        .given('user 123 has orders')
        .uponReceiving('a request for user orders')
        .withRequest({
          method: 'GET',
          path: '/users/123/orders',
          query: {
            limit: '10',
            offset: '0',
          },
        })
        .willRespondWith({
          status: 200,
          body: {
            orders: eachLike({
              id: like('order-1'),
              total: like(99.99),
              status: like('completed'),
              createdAt: iso8601DateTimeWithMillis(),
            }),
            total: like(5),
            hasMore: like(false),
          },
        })
        .executeTest(async (mockServer) => {
          const response = await userService.getUserOrders('123', {
            limit: 10,
            offset: 0,
          });

          expect(response.orders).toBeDefined();
          expect(Array.isArray(response.orders)).toBe(true);
          expect(response.total).toBeDefined();
        });
    });
  });
});
typescript
// tests/pact/user-service.pact.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { UserService } from '../../src/services/UserService';

const { like, eachLike, iso8601DateTimeWithMillis } = MatchersV3;

const provider = new PactV3({
  consumer: 'OrderService',
  provider: 'UserService',
  port: 1234,
  dir: './pacts',
});

describe('User Service Contract', () => {
  const userService = new UserService('http://localhost:1234');

  describe('GET /users/:id', () => {
    test('returns user when found', async () => {
      await provider
        .given('user with ID 123 exists')
        .uponReceiving('a request for user 123')
        .withRequest({
          method: 'GET',
          path: '/users/123',
          headers: {
            Authorization: like('Bearer token'),
          },
        })
        .willRespondWith({
          status: 200,
          headers: {
            'Content-Type': 'application/json',
          },
          body: {
            id: like('123'),
            email: like('user@example.com'),
            name: like('John Doe'),
            age: like(30),
            createdAt: iso8601DateTimeWithMillis('2024-01-01T00:00:00.000Z'),
            role: like('user'),
          },
        })
        .executeTest(async (mockServer) => {
          const user = await userService.getUser('123');

          expect(user.id).toBe('123');
          expect(user.email).toBeDefined();
          expect(user.name).toBeDefined();
        });
    });

    test('returns 404 when user not found', async () => {
      await provider
        .given('user with ID 999 does not exist')
        .uponReceiving('a request for non-existent user')
        .withRequest({
          method: 'GET',
          path: '/users/999',
        })
        .willRespondWith({
          status: 404,
          headers: {
            'Content-Type': 'application/json',
          },
          body: {
            error: like('User not found'),
            code: like('USER_NOT_FOUND'),
          },
        })
        .executeTest(async (mockServer) => {
          await expect(userService.getUser('999')).rejects.toThrow(
            'User not found'
          );
        });
    });
  });

  describe('POST /users', () => {
    test('creates new user', async () => {
      await provider
        .given('user does not exist')
        .uponReceiving('a request to create user')
        .withRequest({
          method: 'POST',
          path: '/users',
          headers: {
            'Content-Type': 'application/json',
          },
          body: {
            email: like('newuser@example.com'),
            name: like('New User'),
            age: like(25),
          },
        })
        .willRespondWith({
          status: 201,
          headers: {
            'Content-Type': 'application/json',
          },
          body: {
            id: like('new-123'),
            email: like('newuser@example.com'),
            name: like('New User'),
            age: like(25),
            createdAt: iso8601DateTimeWithMillis(),
            role: 'user',
          },
        })
        .executeTest(async (mockServer) => {
          const user = await userService.createUser({
            email: 'newuser@example.com',
            name: 'New User',
            age: 25,
          });

          expect(user.id).toBeDefined();
          expect(user.email).toBe('newuser@example.com');
        });
    });
  });

  describe('GET /users/:id/orders', () => {
    test('returns user orders', async () => {
      await provider
        .given('user 123 has orders')
        .uponReceiving('a request for user orders')
        .withRequest({
          method: 'GET',
          path: '/users/123/orders',
          query: {
            limit: '10',
            offset: '0',
          },
        })
        .willRespondWith({
          status: 200,
          body: {
            orders: eachLike({
              id: like('order-1'),
              total: like(99.99),
              status: like('completed'),
              createdAt: iso8601DateTimeWithMillis(),
            }),
            total: like(5),
            hasMore: like(false),
          },
        })
        .executeTest(async (mockServer) => {
          const response = await userService.getUserOrders('123', {
            limit: 10,
            offset: 0,
          });

          expect(response.orders).toBeDefined();
          expect(Array.isArray(response.orders)).toBe(true);
          expect(response.total).toBeDefined();
        });
    });
  });
});

Provider Test (Verify Contract)

提供者测试(验证契约)

typescript
// tests/pact/user-service.provider.test.ts
import { Verifier } from '@pact-foundation/pact';
import path from 'path';
import { app } from '../../src/app';
import { setupTestDB, teardownTestDB } from '../helpers/db';

describe('Pact Provider Verification', () => {
  let server;

  beforeAll(async () => {
    await setupTestDB();
    server = app.listen(3001);
  });

  afterAll(async () => {
    await teardownTestDB();
    server.close();
  });

  test('validates the expectations of OrderService', () => {
    return new Verifier({
      provider: 'UserService',
      providerBaseUrl: 'http://localhost:3001',
      pactUrls: [
        path.resolve(__dirname, '../../pacts/orderservice-userservice.json'),
      ],
      // Provider state setup
      stateHandlers: {
        'user with ID 123 exists': async () => {
          await createTestUser({ id: '123', name: 'John Doe' });
        },
        'user with ID 999 does not exist': async () => {
          await deleteUser('999');
        },
        'user 123 has orders': async () => {
          await createTestUser({ id: '123' });
          await createTestOrder({ userId: '123' });
        },
      },
    })
      .verifyProvider()
      .then((output) => {
        console.log('Pact Verification Complete!');
      });
  });
});
typescript
// tests/pact/user-service.provider.test.ts
import { Verifier } from '@pact-foundation/pact';
import path from 'path';
import { app } from '../../src/app';
import { setupTestDB, teardownTestDB } from '../helpers/db';

describe('Pact Provider Verification', () => {
  let server;

  beforeAll(async () => {
    await setupTestDB();
    server = app.listen(3001);
  });

  afterAll(async () => {
    await teardownTestDB();
    server.close();
  });

  test('validates the expectations of OrderService', () => {
    return new Verifier({
      provider: 'UserService',
      providerBaseUrl: 'http://localhost:3001',
      pactUrls: [
        path.resolve(__dirname, '../../pacts/orderservice-userservice.json'),
      ],
      // Provider state setup
      stateHandlers: {
        'user with ID 123 exists': async () => {
          await createTestUser({ id: '123', name: 'John Doe' });
        },
        'user with ID 999 does not exist': async () => {
          await deleteUser('999');
        },
        'user 123 has orders': async () => {
          await createTestUser({ id: '123' });
          await createTestOrder({ userId: '123' });
        },
      },
    })
      .verifyProvider()
      .then((output) => {
        console.log('Pact Verification Complete!');
      });
  });
});

2. OpenAPI Schema Validation

2. OpenAPI Schema验证

typescript
// tests/contract/openapi.test.ts
import request from 'supertest';
import { app } from '../../src/app';
import OpenAPIValidator from 'express-openapi-validator';
import fs from 'fs';
import yaml from 'js-yaml';

describe('OpenAPI Contract Validation', () => {
  let validator;

  beforeAll(() => {
    const spec = yaml.load(
      fs.readFileSync('./openapi.yaml', 'utf8')
    );

    validator = OpenAPIValidator.middleware({
      apiSpec: spec,
      validateRequests: true,
      validateResponses: true,
    });
  });

  test('GET /users/:id matches schema', async () => {
    const response = await request(app)
      .get('/users/123')
      .expect(200);

    // Validate against OpenAPI schema
    expect(response.body).toMatchObject({
      id: expect.any(String),
      email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/),
      name: expect.any(String),
      age: expect.any(Number),
      createdAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
    });
  });

  test('POST /users validates request body', async () => {
    const invalidUser = {
      email: 'invalid-email',  // Should fail validation
      name: 'Test',
    };

    await request(app)
      .post('/users')
      .send(invalidUser)
      .expect(400);
  });
});
typescript
// tests/contract/openapi.test.ts
import request from 'supertest';
import { app } from '../../src/app';
import OpenAPIValidator from 'express-openapi-validator';
import fs from 'fs';
import yaml from 'js-yaml';

describe('OpenAPI Contract Validation', () => {
  let validator;

  beforeAll(() => {
    const spec = yaml.load(
      fs.readFileSync('./openapi.yaml', 'utf8')
    );

    validator = OpenAPIValidator.middleware({
      apiSpec: spec,
      validateRequests: true,
      validateResponses: true,
    });
  });

  test('GET /users/:id matches schema', async () => {
    const response = await request(app)
      .get('/users/123')
      .expect(200);

    // Validate against OpenAPI schema
    expect(response.body).toMatchObject({
      id: expect.any(String),
      email: expect.stringMatching(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/),
      name: expect.any(String),
      age: expect.any(Number),
      createdAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
    });
  });

  test('POST /users validates request body', async () => {
    const invalidUser = {
      email: 'invalid-email',  // Should fail validation
      name: 'Test',
    };

    await request(app)
      .post('/users')
      .send(invalidUser)
      .expect(400);
  });
});

3. JSON Schema Validation

3. JSON Schema验证

python
undefined
python
undefined

tests/contract/test_schema_validation.py

tests/contract/test_schema_validation.py

import pytest import jsonschema from jsonschema import validate import json
import pytest import jsonschema from jsonschema import validate import json

Define schemas

Define schemas

USER_SCHEMA = { "type": "object", "required": ["id", "email", "name"], "properties": { "id": {"type": "string"}, "email": {"type": "string", "format": "email"}, "name": {"type": "string"}, "age": {"type": "integer", "minimum": 0, "maximum": 150}, "role": {"type": "string", "enum": ["user", "admin"]}, "createdAt": {"type": "string", "format": "date-time"}, }, "additionalProperties": False }
ORDER_SCHEMA = { "type": "object", "required": ["id", "userId", "total", "status"], "properties": { "id": {"type": "string"}, "userId": {"type": "string"}, "total": {"type": "number", "minimum": 0}, "status": { "type": "string", "enum": ["pending", "paid", "shipped", "delivered", "cancelled"] }, "items": { "type": "array", "items": { "type": "object", "required": ["productId", "quantity", "price"], "properties": { "productId": {"type": "string"}, "quantity": {"type": "integer", "minimum": 1}, "price": {"type": "number", "minimum": 0}, } } } } }
class TestAPIContracts: def test_get_user_response_schema(self, api_client): """Validate user endpoint response against schema.""" response = api_client.get('/api/users/123')
    assert response.status_code == 200
    data = response.json()

    # Validate against schema
    validate(instance=data, schema=USER_SCHEMA)

def test_create_user_request_schema(self, api_client):
    """Validate create user request body."""
    valid_user = {
        "email": "test@example.com",
        "name": "Test User",
        "age": 30,
    }

    response = api_client.post('/api/users', json=valid_user)
    assert response.status_code == 201

    # Response should also match schema
    validate(instance=response.json(), schema=USER_SCHEMA)

def test_invalid_request_rejected(self, api_client):
    """Invalid requests should be rejected."""
    invalid_user = {
        "email": "not-an-email",
        "age": -5,  # Invalid age
    }

    response = api_client.post('/api/users', json=invalid_user)
    assert response.status_code == 400

def test_order_response_schema(self, api_client):
    """Validate order endpoint response."""
    response = api_client.get('/api/orders/order-123')

    assert response.status_code == 200
    validate(instance=response.json(), schema=ORDER_SCHEMA)

def test_order_items_array_validation(self, api_client):
    """Validate nested array schema."""
    order_data = {
        "userId": "user-123",
        "items": [
            {"productId": "prod-1", "quantity": 2, "price": 29.99},
            {"productId": "prod-2", "quantity": 1, "price": 49.99},
        ]
    }

    response = api_client.post('/api/orders', json=order_data)
    assert response.status_code == 201

    result = response.json()
    validate(instance=result, schema=ORDER_SCHEMA)
undefined
USER_SCHEMA = { "type": "object", "required": ["id", "email", "name"], "properties": { "id": {"type": "string"}, "email": {"type": "string", "format": "email"}, "name": {"type": "string"}, "age": {"type": "integer", "minimum": 0, "maximum": 150}, "role": {"type": "string", "enum": ["user", "admin"]}, "createdAt": {"type": "string", "format": "date-time"}, }, "additionalProperties": False }
ORDER_SCHEMA = { "type": "object", "required": ["id", "userId", "total", "status"], "properties": { "id": {"type": "string"}, "userId": {"type": "string"}, "total": {"type": "number", "minimum": 0}, "status": { "type": "string", "enum": ["pending", "paid", "shipped", "delivered", "cancelled"] }, "items": { "type": "array", "items": { "type": "object", "required": ["productId", "quantity", "price"], "properties": { "productId": {"type": "string"}, "quantity": {"type": "integer", "minimum": 1}, "price": {"type": "number", "minimum": 0}, } } } } }
class TestAPIContracts: def test_get_user_response_schema(self, api_client): """Validate user endpoint response against schema.""" response = api_client.get('/api/users/123')
    assert response.status_code == 200
    data = response.json()

    # Validate against schema
    validate(instance=data, schema=USER_SCHEMA)

def test_create_user_request_schema(self, api_client):
    """Validate create user request body."""
    valid_user = {
        "email": "test@example.com",
        "name": "Test User",
        "age": 30,
    }

    response = api_client.post('/api/users', json=valid_user)
    assert response.status_code == 201

    # Response should also match schema
    validate(instance=response.json(), schema=USER_SCHEMA)

def test_invalid_request_rejected(self, api_client):
    """Invalid requests should be rejected."""
    invalid_user = {
        "email": "not-an-email",
        "age": -5,  # Invalid age
    }

    response = api_client.post('/api/users', json=invalid_user)
    assert response.status_code == 400

def test_order_response_schema(self, api_client):
    """Validate order endpoint response."""
    response = api_client.get('/api/orders/order-123')

    assert response.status_code == 200
    validate(instance=response.json(), schema=ORDER_SCHEMA)

def test_order_items_array_validation(self, api_client):
    """Validate nested array schema."""
    order_data = {
        "userId": "user-123",
        "items": [
            {"productId": "prod-1", "quantity": 2, "price": 29.99},
            {"productId": "prod-2", "quantity": 1, "price": 49.99},
        ]
    }

    response = api_client.post('/api/orders', json=order_data)
    assert response.status_code == 201

    result = response.json()
    validate(instance=result, schema=ORDER_SCHEMA)
undefined

4. REST Assured for Java

4. Java的REST Assured实现

java
// ContractTest.java
import io.restassured.RestAssured;
import io.restassured.module.jsv.JsonSchemaValidator;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

public class UserAPIContractTest {

    @Test
    public void getUserShouldMatchSchema() {
        given()
            .pathParam("id", "123")
        .when()
            .get("/api/users/{id}")
        .then()
            .statusCode(200)
            .body(JsonSchemaValidator.matchesJsonSchemaInClasspath("schemas/user-schema.json"))
            .body("id", notNullValue())
            .body("email", matchesPattern("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"))
            .body("age", greaterThanOrEqualTo(0));
    }

    @Test
    public void createUserShouldValidateRequest() {
        String userJson = """
            {
                "email": "test@example.com",
                "name": "Test User",
                "age": 30
            }
            """;

        given()
            .contentType("application/json")
            .body(userJson)
        .when()
            .post("/api/users")
        .then()
            .statusCode(201)
            .body("id", notNullValue())
            .body("email", equalTo("test@example.com"))
            .body("createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T.*"));
    }

    @Test
    public void getUserOrdersShouldReturnArray() {
        given()
            .pathParam("id", "123")
            .queryParam("limit", 10)
        .when()
            .get("/api/users/{id}/orders")
        .then()
            .statusCode(200)
            .body("orders", isA(java.util.List.class))
            .body("orders[0].id", notNullValue())
            .body("orders[0].status", isIn(Arrays.asList(
                "pending", "paid", "shipped", "delivered", "cancelled"
            )))
            .body("total", greaterThanOrEqualTo(0));
    }

    @Test
    public void invalidRequestShouldReturn400() {
        String invalidUser = """
            {
                "email": "not-an-email",
                "age": -5
            }
            """;

        given()
            .contentType("application/json")
            .body(invalidUser)
        .when()
            .post("/api/users")
        .then()
            .statusCode(400)
            .body("error", notNullValue());
    }
}
java
// ContractTest.java
import io.restassured.RestAssured;
import io.restassured.module.jsv.JsonSchemaValidator;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

public class UserAPIContractTest {

    @Test
    public void getUserShouldMatchSchema() {
        given()
            .pathParam("id", "123")
        .when()
            .get("/api/users/{id}")
        .then()
            .statusCode(200)
            .body(JsonSchemaValidator.matchesJsonSchemaInClasspath("schemas/user-schema.json"))
            .body("id", notNullValue())
            .body("email", matchesPattern("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"))
            .body("age", greaterThanOrEqualTo(0));
    }

    @Test
    public void createUserShouldValidateRequest() {
        String userJson = """
            {
                "email": "test@example.com",
                "name": "Test User",
                "age": 30
            }
            """;

        given()
            .contentType("application/json")
            .body(userJson)
        .when()
            .post("/api/users")
        .then()
            .statusCode(201)
            .body("id", notNullValue())
            .body("email", equalTo("test@example.com"))
            .body("createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T.*"));
    }

    @Test
    public void getUserOrdersShouldReturnArray() {
        given()
            .pathParam("id", "123")
            .queryParam("limit", 10)
        .when()
            .get("/api/users/{id}/orders")
        .then()
            .statusCode(200)
            .body("orders", isA(java.util.List.class))
            .body("orders[0].id", notNullValue())
            .body("orders[0].status", isIn(Arrays.asList(
                "pending", "paid", "shipped", "delivered", "cancelled"
            )))
            .body("total", greaterThanOrEqualTo(0));
    }

    @Test
    public void invalidRequestShouldReturn400() {
        String invalidUser = """
            {
                "email": "not-an-email",
                "age": -5
            }
            """;

        given()
            .contentType("application/json")
            .body(invalidUser)
        .when()
            .post("/api/users")
        .then()
            .statusCode(400)
            .body("error", notNullValue());
    }
}

5. Contract Testing with Postman

5. 使用Postman进行契约测试

json
// postman-collection.json
{
  "info": {
    "name": "User API Contract Tests"
  },
  "item": [
    {
      "name": "Get User",
      "request": {
        "method": "GET",
        "url": "{{baseUrl}}/users/{{userId}}"
      },
      "test": "
        pm.test('Response status is 200', () => {
          pm.response.to.have.status(200);
        });

        pm.test('Response matches schema', () => {
          const schema = {
            type: 'object',
            required: ['id', 'email', 'name'],
            properties: {
              id: { type: 'string' },
              email: { type: 'string', format: 'email' },
              name: { type: 'string' },
              age: { type: 'integer' }
            }
          };

          pm.response.to.have.jsonSchema(schema);
        });

        pm.test('Email format is valid', () => {
          const data = pm.response.json();
          pm.expect(data.email).to.match(/^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$/);
        });
      "
    }
  ]
}
json
// postman-collection.json
{
  "info": {
    "name": "User API Contract Tests"
  },
  "item": [
    {
      "name": "Get User",
      "request": {
        "method": "GET",
        "url": "{{baseUrl}}/users/{{userId}}"
      },
      "test": "
        pm.test('Response status is 200', () => {
          pm.response.to.have.status(200);
        });

        pm.test('Response matches schema', () => {
          const schema = {
            type: 'object',
            required: ['id', 'email', 'name'],
            properties: {
              id: { type: 'string' },
              email: { type: 'string', format: 'email' },
              name: { type: 'string' },
              age: { type: 'integer' }
            }
          };

          pm.response.to.have.jsonSchema(schema);
        });

        pm.test('Email format is valid', () => {
          const data = pm.response.json();
          pm.expect(data.email).to.match(/^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$/);
        });
      "
    }
  ]
}

6. Pact Broker Integration

6. Pact Broker集成

yaml
undefined
yaml
undefined

.github/workflows/contract-tests.yml

.github/workflows/contract-tests.yml

name: Contract Tests
on: [push, pull_request]
jobs: consumer-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3
  - run: npm ci
  - run: npm run test:pact

  - name: Publish Pacts
    run: |
      npx pact-broker publish ./pacts \
        --consumer-app-version=${{ github.sha }} \
        --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
        --broker-token=${{ secrets.PACT_BROKER_TOKEN }}
provider-tests: runs-on: ubuntu-latest needs: consumer-tests steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3
  - run: npm ci
  - run: npm run test:pact:provider

  - name: Can I Deploy?
    run: |
      npx pact-broker can-i-deploy \
        --pacticipant=UserService \
        --version=${{ github.sha }} \
        --to-environment=production \
        --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
        --broker-token=${{ secrets.PACT_BROKER_TOKEN }}
undefined
name: Contract Tests
on: [push, pull_request]
jobs: consumer-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3
  - run: npm ci
  - run: npm run test:pact

  - name: Publish Pacts
    run: |
      npx pact-broker publish ./pacts \
        --consumer-app-version=${{ github.sha }} \
        --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
        --broker-token=${{ secrets.PACT_BROKER_TOKEN }}
provider-tests: runs-on: ubuntu-latest needs: consumer-tests steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3
  - run: npm ci
  - run: npm run test:pact:provider

  - name: Can I Deploy?
    run: |
      npx pact-broker can-i-deploy \
        --pacticipant=UserService \
        --version=${{ github.sha }} \
        --to-environment=production \
        --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
        --broker-token=${{ secrets.PACT_BROKER_TOKEN }}
undefined

Best Practices

最佳实践

✅ DO

✅ 建议

  • Test contracts from consumer perspective
  • Use matchers for flexible matching
  • Validate schema structure, not specific values
  • Version your contracts
  • Test error responses
  • Use Pact broker for contract sharing
  • Run contract tests in CI
  • Test backward compatibility
  • 从消费者视角测试契约
  • 使用匹配器实现灵活匹配
  • 验证Schema结构而非具体值
  • 对契约进行版本管理
  • 测试错误响应
  • 使用Pact Broker共享契约
  • 在CI流程中运行契约测试
  • 测试向后兼容性

❌ DON'T

❌ 避免

  • Test business logic in contract tests
  • Hard-code specific values in contracts
  • Skip error scenarios
  • Test UI in contract tests
  • Ignore contract versioning
  • Deploy without contract verification
  • Test implementation details
  • Mock contract tests
  • 在契约测试中测试业务逻辑
  • 在契约中硬编码具体值
  • 跳过错误场景测试
  • 在契约测试中测试UI
  • 忽略契约版本管理
  • 未验证契约就部署
  • 测试实现细节
  • 对契约测试进行模拟

Tools

工具

  • Pact: Consumer-driven contracts (multiple languages)
  • Spring Cloud Contract: JVM contract testing
  • OpenAPI/Swagger: API specification and validation
  • Postman: API contract testing
  • REST Assured: Java API testing
  • Dredd: OpenAPI/API Blueprint testing
  • Spectral: OpenAPI linting
  • Pact:多语言支持的消费者驱动契约工具
  • Spring Cloud Contract:JVM平台契约测试工具
  • OpenAPI/Swagger:API规范与验证工具
  • Postman:API契约测试工具
  • REST Assured:Java API测试工具
  • Dredd:OpenAPI/API Blueprint测试工具
  • Spectral:OpenAPI语法检查工具

Examples

扩展参考

See also: integration-testing, api-versioning-strategy, continuous-testing for comprehensive API testing strategies.
更多全面的API测试策略可参考:集成测试、API版本控制策略、持续测试相关内容。