contract-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Contract Testing

契约测试

Pact (Consumer-Driven Contracts)

Pact(消费者驱动契约)

Consumer Test (Node.js)

消费者侧测试(Node.js)

typescript
import { PactV3, MatchersV3 } from '@pact-foundation/pact';

const { like, eachLike, string, integer } = MatchersV3;

const provider = new PactV3({
  consumer: 'OrderService',
  provider: 'ProductService',
});

describe('Product API contract', () => {
  it('returns a product by ID', async () => {
    await provider
      .given('product 123 exists')
      .uponReceiving('a request for product 123')
      .withRequest({
        method: 'GET',
        path: '/api/products/123',
        headers: { Accept: 'application/json' },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: like({
          id: string('123'),
          name: string('Widget'),
          price: integer(1999),
        }),
      })
      .executeTest(async (mockserver) => {
        const client = new ProductClient(mockserver.url);
        const product = await client.getProduct('123');
        expect(product.id).toBe('123');
        expect(product.name).toBeDefined();
      });
  });
});
typescript
import { PactV3, MatchersV3 } from '@pact-foundation/pact';

const { like, eachLike, string, integer } = MatchersV3;

const provider = new PactV3({
  consumer: 'OrderService',
  provider: 'ProductService',
});

describe('Product API contract', () => {
  it('returns a product by ID', async () => {
    await provider
      .given('product 123 exists')
      .uponReceiving('a request for product 123')
      .withRequest({
        method: 'GET',
        path: '/api/products/123',
        headers: { Accept: 'application/json' },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: like({
          id: string('123'),
          name: string('Widget'),
          price: integer(1999),
        }),
      })
      .executeTest(async (mockserver) => {
        const client = new ProductClient(mockserver.url);
        const product = await client.getProduct('123');
        expect(product.id).toBe('123');
        expect(product.name).toBeDefined();
      });
  });
});

Provider Verification

提供者侧验证

typescript
import { Verifier } from '@pact-foundation/pact';

describe('Product provider verification', () => {
  it('validates contracts', async () => {
    await new Verifier({
      providerBaseUrl: 'http://localhost:3000',
      pactUrls: ['./pacts/OrderService-ProductService.json'],
      // Or from broker:
      // pactBrokerUrl: 'https://pact-broker.example.com',
      // publishVerificationResult: true,
      // providerVersion: process.env.GIT_SHA,
      stateHandlers: {
        'product 123 exists': async () => {
          await db.product.create({ data: { id: '123', name: 'Widget', price: 1999 } });
        },
      },
    }).verifyProvider();
  });
});
typescript
import { Verifier } from '@pact-foundation/pact';

describe('Product provider verification', () => {
  it('validates contracts', async () => {
    await new Verifier({
      providerBaseUrl: 'http://localhost:3000',
      pactUrls: ['./pacts/OrderService-ProductService.json'],
      // Or from broker:
      // pactBrokerUrl: 'https://pact-broker.example.com',
      // publishVerificationResult: true,
      // providerVersion: process.env.GIT_SHA,
      stateHandlers: {
        'product 123 exists': async () => {
          await db.product.create({ data: { id: '123', name: 'Widget', price: 1999 } });
        },
      },
    }).verifyProvider();
  });
});

Spring Cloud Contract

Spring Cloud Contract

Contract Definition (Groovy DSL)

契约定义(Groovy DSL)

groovy
// contracts/shouldReturnProduct.groovy
Contract.make {
    description "should return product by ID"
    request {
        method GET()
        url "/api/products/123"
        headers { contentType applicationJson() }
    }
    response {
        status OK()
        headers { contentType applicationJson() }
        body([
            id: "123",
            name: $(regex('[A-Za-z ]+')),
            price: $(regex('[0-9]+')),
        ])
    }
}
groovy
// contracts/shouldReturnProduct.groovy
Contract.make {
    description "should return product by ID"
    request {
        method GET()
        url "/api/products/123"
        headers { contentType applicationJson() }
    }
    response {
        status OK()
        headers { contentType applicationJson() }
        body([
            id: "123",
            name: $(regex('[A-Za-z ]+')),
            price: $(regex('[0-9]+')),
        ])
    }
}

Provider Base Test

提供者基础测试

java
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
public abstract class ContractBaseTest {
    @Autowired MockMvc mockMvc;
    @MockBean ProductRepository productRepo;

    @BeforeEach
    void setup() {
        when(productRepo.findById("123"))
            .thenReturn(Optional.of(new Product("123", "Widget", 1999)));
        RestAssuredMockMvc.mockMvc(mockMvc);
    }
}
java
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
public abstract class ContractBaseTest {
    @Autowired MockMvc mockMvc;
    @MockBean ProductRepository productRepo;

    @BeforeEach
    void setup() {
        when(productRepo.findById("123"))
            .thenReturn(Optional.of(new Product("123", "Widget", 1999)));
        RestAssuredMockMvc.mockMvc(mockMvc);
    }
}

Workflow

工作流程

1. Consumer writes contract test → generates pact file
2. Pact file published to broker (or shared via file)
3. Provider runs verification against pact
4. Both sides deploy only when contracts pass
1. 消费者编写契约测试 → 生成pact文件
2. Pact文件发布到broker(或通过文件共享)
3. 提供者针对pact执行验证
4. 只有契约测试通过后双方才可部署

Pact Broker

Pact Broker

bash
undefined
bash
undefined

Publish pact

Publish pact

npx pact-broker publish ./pacts
--consumer-app-version=$(git rev-parse HEAD)
--broker-base-url=https://pact-broker.example.com
npx pact-broker publish ./pacts
--consumer-app-version=$(git rev-parse HEAD)
--broker-base-url=https://pact-broker.example.com

Can I Deploy?

Can I Deploy?

npx pact-broker can-i-deploy
--pacticipant=OrderService
--version=$(git rev-parse HEAD)
--to-environment=production
undefined
npx pact-broker can-i-deploy
--pacticipant=OrderService
--version=$(git rev-parse HEAD)
--to-environment=production
undefined

Anti-Patterns

反模式

Anti-PatternFix
Testing implementation detailsTest contract shape, not business logic
Over-specifying response fieldsUse matchers (like, regex), not exact values
No state managementDefine provider states for test data setup
Contracts not in CIRun contract tests in CI/CD pipeline
Skipping provider verificationBoth sides must run their tests
反模式修复方案
测试实现细节测试契约结构,而非业务逻辑
过度指定响应字段使用匹配器(like、regex),而非固定值
无状态管理定义提供者状态用于测试数据准备
契约未接入CI在CI/CD流水线中运行契约测试
跳过提供者验证双方都必须执行对应的测试

Production Checklist

上线检查清单

  • Consumer-driven contracts for all API dependencies
  • Pact Broker (or equivalent) for contract sharing
  • Provider verification in CI pipeline
  • can-i-deploy
    check before deployment
  • Provider states for test data setup
  • Contract versioned with git SHA
  • 所有API依赖都实现了消费者驱动契约
  • 用于契约共享的Pact Broker(或等效工具)
  • CI流水线中包含提供者验证步骤
  • 部署前执行
    can-i-deploy
    检查
  • 配置了用于测试数据准备的提供者状态
  • 契约使用git SHA进行版本管理