Kiểm thử hợp đồng Microservices với Pact & Node.js

Table of Contents
Việc lựa chọn chiến lược kiểm thử API phù hợp sẽ quyết định liệu các bản phát hành microservice có được thực hiện trơn tru hay thất bại trong quá trình chuyển đổi triển khai sản xuất. Kiểm thử hợp đồng theo hướng người tiêu dùng loại bỏ các tích hợp API bị lỗi trên các microservice mà không cần phải thiết lập các cụm môi trường end-to-end nặng nề. Sử dụng Pact trong các dịch vụ Node.js cho phép các ứng dụng người tiêu dùng định nghĩa các hợp đồng tương tác HTTP mà các nhà cung cấp xác minh độc lập trong các pipeline tích hợp liên tục, cho phép triển khai độc lập an toàn.
Tại sao kiểm thử hợp đồng theo hướng người tiêu dùng lại cần thiết cho API?
Kiến trúc microservice thường gặp vấn đề với các bộ kiểm thử tích hợp dễ vỡ dựa vào môi trường staging trực tiếp. Khi nhóm A sửa đổi một lược đồ endpoint API trong dịch vụ A, dịch vụ người tiêu dùng B ở hạ nguồn thường bị lỗi trong thời gian chạy vì các kiểm thử tích hợp end-to-end đã bị bỏ qua hoặc được thực hiện sau khi triển khai.

Kiểm thử hợp đồng theo hướng người tiêu dùng đảo ngược quy trình xác minh API truyền thống bằng cách cho phép người tiêu dùng API viết các kiểm thử định nghĩa chính xác các kỳ vọng về yêu cầu và phản hồi của họ. Những kỳ vọng này tạo ra một tệp hợp đồng JSON tiêu chuẩn hóa (tệp Pact). Dịch vụ nhà cung cấp sau đó phát lại các yêu cầu đã ghi này đối với phiên bản cục bộ của chính nó, xác minh rằng các phản hồi của nó đáp ứng hợp đồng mà không cần dịch vụ người tiêu dùng phải trực tuyến.
// Example: Pact V4 consumer contract test in Node.js using @pact-foundation/pact
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
import { fetchUserProfile } from '../src/api-client';
const { like, string, integer } = MatchersV3;
const provider = new PactV4({
consumer: 'OrderWebClient',
provider: 'UserService',
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('User Service Contract Tests', () => {
it('returns user profile details for a valid user ID', async () => {
await provider.addInteraction({
states: [{ description: 'user with ID 101 exists' }],
uponReceiving: 'a request for user profile 101',
withRequest: {
method: 'GET',
path: '/api/v1/users/101',
headers: { Accept: 'application/json' },
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: integer(101),
name: string('Jane Doe'),
email: like('jane.doe@example.com'),
role: string('admin'),
},
},
});
await provider.executeTest(async (mockServer) => {
const user = await fetchUserProfile(mockServer.url, 101);
expect(user.name).toEqual('Jane Doe');
expect(user.role).toEqual('admin');
});
});
});
Hiểu được luồng theo hướng người tiêu dùng này làm nổi bật lý do tại sao các kiểm thử hợp đồng chạy nhanh hơn đáng kể so với các kiểm thử end-to-end của trình duyệt. Vì kiểm thử người tiêu dùng được thực thi đối với một máy chủ HTTP mock Pact cục bộ, việc thực thi kiểm thử hoàn tất trong vài mili giây mà không có độ trễ mạng hoặc chi phí khởi tạo cơ sở dữ liệu.
// Client implementation verified by the Pact consumer test
import axios from 'axios';
export interface UserProfile {
id: number;
name: string;
email: string;
role: string;
}
export async function fetchUserProfile(baseUrl: string, userId: number): Promise<UserProfile> {
const response = await axios.get(`${baseUrl}/api/v1/users/${userId}`, {
headers: { Accept: 'application/json' },
});
return response.data;
}
Tệp JSON Pact được tạo chứa các thông số kỹ thuật rõ ràng cho tiêu đề yêu cầu HTTP, tham số truy vấn, đường dẫn URL và các quy tắc khớp nội dung linh hoạt. Các bộ so khớp linh hoạt như like() và integer() hướng dẫn trình xác minh nhà cung cấp kiểm tra các kiểu dữ liệu thay vì các giá trị chuỗi được mã hóa cứng.
Ngoài các tương tác HTTP REST, các kiến trúc đám mây hiện đại cũng giao tiếp thông qua các bus sự kiện không đồng bộ. Pact cung cấp khả năng kiểm thử hợp đồng tin nhắn cho các microservice hướng sự kiện sử dụng RabbitMQ, Apache Kafka hoặc các hàng đợi AWS SNS và SQS. Các ứng dụng người tiêu dùng định nghĩa lược đồ tải trọng của các tin nhắn sự kiện đến, cho phép các nhà sản xuất xác minh định dạng tải trọng sự kiện trước khi xuất bản các sự kiện đến các message broker sản xuất.
// Asynchronous message contract testing example with Pact
import { MessageConsumerPact, Matchers } from '@pact-foundation/pact';
const messageProvider = new MessageConsumerPact({
consumer: 'NotificationService',
provider: 'OrderEventProducer',
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('Order Created Event Contract', () => {
it('handles order created domain events correctly', async () => {
await messageProvider
.given('order 4004 was created')
.expectsToReceive('an order created event payload')
.withContent({
orderId: Matchers.like('ORD-4004'),
amount: Matchers.decimal(149.99),
customerEmail: Matchers.like('customer@example.com'),
})
.verify(async (message) => {
// Verify message handler processes event object cleanly
const parsed = JSON.parse(message.contents.toString());
expect(parsed.orderId).toBeDefined();
});
});
});
Làm thế nào để bạn định nghĩa các kỳ vọng của người tiêu dùng Pact trong Node.js?
Việc định nghĩa các kỳ vọng của người tiêu dùng Pact yêu cầu sử dụng các bộ so khớp an toàn kiểu cho phép các nhà cung cấp linh hoạt trong khi thực thi các hợp đồng cấu trúc tải trọng. Sử dụng so khớp bằng nghiêm ngặt cho các chuỗi dấu thời gian hoặc ID được tạo sẽ gây ra lỗi xác minh nhà cung cấp khi các giá trị tự động tăng của cơ sở dữ liệu thay đổi.

Pact V4 cung cấp một DSL so khớp phong phú bao gồm like(), eachLike(), regex() và datetime(). Các bộ so khớp này đảm bảo rằng các kiểm thử người tiêu dùng xác minh các lược đồ cấu trúc thay vì các giá trị tạm thời cụ thể.
// Advanced Pact V4 consumer test with array and regex matchers
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
import { fetchOrderHistory } from '../src/order-client';
const { eachLike, string, decimal, regex } = MatchersV3;
const provider = new PactV4({
consumer: 'DashboardApp',
provider: 'OrderService',
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('Order Service List Contract', () => {
it('returns a list of recent customer orders', async () => {
await provider.addInteraction({
states: [{ description: 'customer 502 has existing orders' }],
uponReceiving: 'a request for customer order history',
withRequest: {
method: 'GET',
path: '/api/v1/customers/502/orders',
query: { limit: '5' },
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
customerId: 502,
orders: eachLike({
orderId: regex(/^ORD-\d{6}$/, 'ORD-123456'),
totalAmount: decimal(99.95),
status: regex(/^(PENDING|COMPLETED|SHIPPED)$/, 'COMPLETED'),
}),
},
},
});
await provider.executeTest(async (mockServer) => {
const history = await fetchOrderHistory(mockServer.url, 502, 5);
expect(history.orders.length).toBeGreaterThan(0);
});
});
});
Sử dụng eachLike() khẳng định rằng phản hồi trả về chứa một mảng các đối tượng trong đó mỗi phần tử khớp với mẫu cấu trúc được chỉ định. Mẫu này cho phép nhà cung cấp trả về một mục hoặc năm mươi mục mà không làm hỏng khẳng định kiểm thử hợp đồng.
| Loại bộ so khớp | Ví dụ sử dụng | Quy tắc xác minh | Trường hợp sử dụng |
|---|---|---|---|
like(val) | like('user@example.com') | Khớp kiểu dữ liệu của giá trị | Các trường chuỗi/số chung |
eachLike(template) | eachLike({ id: 1 }) | Khớp mảng trong đó các phần tử tuân theo mẫu | Phản hồi danh sách được phân trang |
regex(pattern, sample) | regex(/^\d{4}-\d{2}$/, '2026-07') | So khớp chuỗi Regex | Ngày ISO, UUID, ID tùy chỉnh |
integer(sample) | integer(42) | Khớp kiểu số nguyên | Khóa chính cơ sở dữ liệu |
Việc cấu trúc các hợp đồng xung quanh các yêu cầu miền nghiệp vụ ngăn chặn việc đặc tả quá mức. Người tiêu dùng chỉ nên ký hợp đồng cho các trường JSON cụ thể mà họ thực sự đọc và xử lý, cho phép các nhà cung cấp thêm các trường mới vào tải trọng phản hồi mà không làm mất hiệu lực các hợp đồng hiện có.
// Custom header matchers preventing rigid authorization token failures
export const authenticatedHeadersContract = {
Authorization: regex(/^Bearer [A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'),
'Content-Type': 'application/json',
};
Làm thế nào để thực hiện xác minh nhà cung cấp tự động trong CI Pipelines?
Xác minh nhà cung cấp là bước mà dịch vụ nhà sản xuất API tải xuống các hợp đồng đã xuất bản từ Pact Broker và thực thi chúng đối với máy chủ HTTP cục bộ của nó. Pact Verifier gửi các yêu cầu được chỉ định trong hợp đồng đến ứng dụng nhà cung cấp và khẳng định rằng các phản hồi thực tế khớp với mã trạng thái, tiêu đề và cấu trúc tải trọng dự kiến.

Việc thực hiện xác minh nhà cung cấp yêu cầu thiết lập các trình xử lý trạng thái nhà cung cấp trong Node.js. Các trình xử lý trạng thái chuẩn bị cơ sở dữ liệu hoặc trạng thái bộ nhớ nội bộ của ứng dụng nhà cung cấp trước khi mỗi yêu cầu tương tác được trình xác minh thực thi.
// Example: Provider verification test script using @pact-foundation/pact Verifier
import { Verifier } from '@pact-foundation/pact';
import { createServer } from '../src/app';
import { Server } from 'http';
import { seedTestDatabase } from '../test/db-helpers';
describe('Pact Provider Verification', () => {
let server: Server;
const PORT = 8088;
beforeAll((done) => {
const app = createServer();
server = app.listen(PORT, () => done());
});
afterAll((done) => {
server.close(() => done());
});
it('validates contract specs against local provider server', async () => {
const verifier = new Verifier({
provider: 'UserService',
providerBaseUrl: `http://localhost:${PORT}`,
pactBrokerUrl: process.env.PACT_BROKER_URL || 'https://broker.example.com',
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
publishVerificationResult: process.env.CI === 'true',
providerVersion: process.env.GIT_COMMIT || '1.0.0',
providerVersionBranch: process.env.GIT_BRANCH || 'main',
stateHandlers: {
'user with ID 101 exists': async () => {
await seedTestDatabase([{ id: 101, name: 'Jane Doe', email: 'jane.doe@example.com', role: 'admin' }]);
return { description: 'State seeded: user 101 created' };
},
},
});
const output = await verifier.verifyProvider();
console.log('Pact verification complete:', output);
});
});
Đặt publishVerificationResult: true trong môi trường CI sẽ xuất bản các lần vượt qua hoặc thất bại xác minh trở lại Pact Broker. Việc theo dõi ma trận xác minh này cho phép các nhóm xác minh khả năng tương thích giữa các commit git của người tiêu dùng cụ thể và các commit git của nhà cung cấp.
// Express app setup snippet supporting test state hooks
import express from 'express';
export function createServer() {
const app = express();
app.use(express.json());
app.get('/api/v1/users/:id', async (req, res) => {
const userId = parseInt(req.params.id, 10);
// Fetch from database seeded by state handler
const user = await findUserInDatabase(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.json(user);
});
return app;
}
async function findUserInDatabase(id: number) {
// Mock DB query implementation
return { id, name: 'Jane Doe', email: 'jane.doe@example.com', role: 'admin' };
}
Việc cấu hình các trình xử lý trạng thái một cách sạch sẽ yêu cầu cách ly các giao dịch cơ sở dữ liệu để ngăn chặn sự can thiệp của kiểm thử. Việc gói gọn mỗi lần chạy xác minh nhà cung cấp trong các lần rollback cơ sở dữ liệu hoặc các phiên bản bộ nhớ SQLite tạm thời đảm bảo rằng các kiểm thử vẫn mang tính xác định bất kể thứ tự thực thi.
// Transactional database rollback wrapper for provider state handlers
export async function withTestTransaction(callback: () => Promise<void>) {
const connection = await db.getConnection();
await connection.beginTransaction();
try {
await callback();
} finally {
await connection.rollback();
connection.release();
}
}
Làm thế nào để xử lý các thay đổi lược đồ gây lỗi và Pact Broker Can-I-Deploy?
Pact Broker hoạt động như kho lưu trữ trung tâm và công cụ ma trận tương thích cho các quy trình kiểm thử hợp đồng. Nó cung cấp một tiện ích CLI có tên can-i-deploy để truy vấn ma trận trước khi triển khai nhằm đảm bảo rằng một phiên bản dịch vụ tương thích với tất cả các phiên bản người tiêu dùng và nhà cung cấp đã triển khai trong các môi trường mục tiêu.

Thực hiện can-i-deploy bên trong các pipeline triển khai hoạt động như một người gác cổng. Nếu một nhóm nhà cung cấp đưa ra một thay đổi gây lỗi làm mất hiệu lực một hợp đồng người tiêu dùng đang hoạt động, can-i-deploy sẽ thất bại ngay lập tức, ngăn chặn việc triển khai tiếp tục.
#!/usr/bin/env bash
# CI Script: Executing Pact Broker can-i-deploy check before staging release
set -euo pipefail
SERVICE_NAME="UserService"
VERSION=$(git rev-parse --short HEAD)
ENVIRONMENT="production"
echo "Checking if ${SERVICE_NAME} version ${VERSION} is safe to deploy to ${ENVIRONMENT}..."
npx @pact-foundation/pact-cli broker can-i-deploy \
--pact-broker-base-url="${PACT_BROKER_URL}" \
--broker-token="${PACT_BROKER_TOKEN}" \
--to-environment="${ENVIRONMENT}" \
--pacticipant="${SERVICE_NAME}" \
--version="${VERSION}"
echo "Deployment check passed! Service is safe to release."
Khi nhà cung cấp phải đưa ra một thay đổi API gây lỗi (chẳng hạn như đổi tên một trường hoặc thay đổi kiểu tham số), quy trình hợp đồng tuân theo một chu kỳ ngừng sử dụng mở rộng. Đầu tiên, người tiêu dùng cập nhật hợp đồng của họ để khai báo hỗ trợ cho trường mới trong khi vẫn duy trì hỗ trợ dự phòng cho trường cũ.
// Versioned migration contract pattern supporting dual schema fields
export function mapUserResponse(data: any) {
return {
id: data.id,
// Accept new field 'fullName' or fall back to legacy 'name'
displayName: data.fullName || data.name,
email: data.email,
};
}
Sau khi người tiêu dùng triển khai mã hợp đồng đã cập nhật của họ vào sản xuất, can-i-deploy xác minh rằng không có người tiêu dùng sản xuất đang hoạt động nào phụ thuộc vào trường đã ngừng sử dụng. Chỉ khi đó nhà cung cấp mới có thể xóa trường cũ một cách an toàn mà không làm gián đoạn lưu lượng sản xuất.
# GitHub Actions workflow for publishing contracts and checking deploy safety
name: Pact Contract Pipeline
on: [push]
jobs:
contract-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Run Consumer Contract Tests
run: npm test -- --grep "Contract"
- name: Publish Contracts to Pact Broker
run: |
npx @pact-foundation/pact-cli broker publish pacts \
--pact-broker-base-url="${{ secrets.PACT_BROKER_URL }}" \
--broker-token="${{ secrets.PACT_BROKER_TOKEN }}" \
--consumer-app-version="${{ github.sha }}" \
--branch="${{ github.ref_name }}"
- name: Can I Deploy Check
run: |
npx @pact-foundation/pact-cli broker can-i-deploy \
--pact-broker-base-url="${{ secrets.PACT_BROKER_URL }}" \
--broker-token="${{ secrets.PACT_BROKER_TOKEN }}" \
--pacticipant="OrderWebClient" \
--version="${{ github.sha }}" \
--to-environment="production"
Việc quản lý các pact đang chờ xử lý trong Pact Broker cho phép các nhà cung cấp kết hợp các hợp đồng người tiêu dùng mới vào các lần chạy CI của họ mà không làm hỏng ngay lập tức các bản dựng của nhà cung cấp. Pact Verifier hỗ trợ các cờ --enable-pending, đánh dấu các hợp đồng mới chưa được xác minh là đang chờ xử lý thay vì chặn các yêu cầu kéo của nhà cung cấp.
Các phương pháp hay nhất để bảo trì Pact ở quy mô lớn là gì?
Việc duy trì các bộ kiểm thử hợp đồng trên hàng chục microservice yêu cầu thiết lập các hướng dẫn rõ ràng về mức độ chi tiết của hợp đồng, quản lý thẻ broker và cách ly trình xử lý trạng thái. Việc giữ các kiểm thử hợp đồng tập trung nghiêm ngặt vào các ranh giới giao thức HTTP đảm bảo khả năng bảo trì bộ kiểm thử lâu dài.
Các kiểm thử hợp đồng không bao giờ được thay thế các kiểm thử đơn vị nội bộ hoặc kiểm thử lớp cơ sở dữ liệu. Tránh định nghĩa các trường hợp biên toàn diện cho các quy tắc xác thực nội bộ bên trong các hợp đồng Pact. Thay vào đó, hãy định nghĩa một hợp đồng thành công và một tập hợp tối thiểu các hợp đồng trạng thái lỗi HTTP (chẳng hạn như 400 Bad Request hoặc 404 Not Found) để giữ cho việc xác minh hợp đồng nhanh chóng.
// Clean state handler isolation module pattern
export const stateHandlers = {
'user 101 exists': async () => {
await db.user.upsert({
where: { id: 101 },
update: { name: 'Jane Doe' },
create: { id: 101, name: 'Jane Doe', email: 'jane@example.com' },
});
},
'user 101 has zero balance': async () => {
await db.account.update({
where: { userId: 101 },
data: { balance: 0.00 },
});
},
};
Kiểm thử hợp đồng theo hướng người tiêu dùng thay đổi cách các tổ chức kỹ thuật phối hợp các thay đổi API. Bằng cách thiết lập các hợp đồng HTTP có thể thực thi bằng máy giữa các microservice Node.js, các nhóm loại bỏ các lỗi tích hợp trong khi vẫn duy trì các pipeline triển khai liên tục độc lập.
Việc áp dụng kiểm thử hợp đồng theo hướng người tiêu dùng tạo ra sự liên kết minh bạch giữa các nhóm. Các nhà phát triển web front-end và kỹ sư API back-end có thể thống nhất về lược đồ tải trọng ngay từ đầu trước khi viết mã triển khai, loại bỏ ma sát tích hợp trong quá trình xem xét yêu cầu kéo.
Việc thiết lập các webhook xác minh hợp đồng bên trong Pact Broker của bạn sẽ tự động kích hoạt các bản dựng xác minh nhà cung cấp bất cứ khi nào người tiêu dùng xuất bản một bản sửa đổi hợp đồng mới. Vòng lặp phản hồi theo thời gian thực này đảm bảo rằng các sự không tương thích lược đồ API được phát hiện vài phút sau khi yêu cầu kéo của người tiêu dùng được hợp nhất.
Việc giám sát liên tục các báo cáo ma trận xác minh trong bảng điều khiển Pact Broker của bạn cung cấp cho các nhà lãnh đạo kỹ thuật khả năng hiển thị về tình trạng phụ thuộc chéo dịch vụ trên các môi trường staging và sản xuất.
Việc tiêu chuẩn hóa các mẫu kiểm thử hợp đồng trên tất cả các microservice Node.js back-end đảm bảo rằng các bổ sung microservice mới áp dụng các mẫu xác minh hợp đồng một cách tự nhiên. Với việc xác minh hợp đồng tự động thực thi khả năng tương thích API, các nhóm kỹ thuật có thể phát hành các bản cập nhật microservice nhiều lần mỗi ngày với sự an tâm tuyệt đối.
Bạn cũng có thể thích
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Các chiến lược Mocking microservices: Hướng dẫn MSW vs WireMock
So sánh chuyên sâu MSW và WireMock cho kiểm thử tích hợp microservices, các mẫu chặn mạng, thiết lập container và fault injection.
Read more
gRPC vs ConnectRPC: Microservices hiện đại và Protobuf gốc trình duyệt
Đánh giá kiến trúc gRPC vs ConnectRPC trong TypeScript và Go, khám phá streaming HTTP/1.1 vs HTTP/2, client trình duyệt không cần proxy Envoy và độ trễ RPC p99.
Read more
WebAssembly Ngoài Trình Duyệt: Xây Dựng Microservices Hiệu Năng Cao
Khám phá cách sử dụng WebAssembly phía máy chủ với Wasmtime, WasmEdge và Spin để xây dựng các microservices tốc độ gần như native, không phụ thuộc ngôn ngữ và được sandbox về khả năng — với các điểm chuẩn thực tế so với Docker containers.
Read more