1. Bối cảnh & oracle: hợp đồng là chân lý giữa các dịch vụ
Chúng ta làm việc trong một ngân hàng số với hàng chục microservice: dịch vụ tài khoản, sổ cái, hạn mức, phòng chống gian lận, thông báo. Một luồng chuyển khoản đơn giản đi qua năm sáu dịch vụ, mỗi dịch vụ do một đội khác nhau phát triển và phát hành độc lập. Vấn đề cốt lõi của kiến trúc này không phải là mỗi dịch vụ chạy đúng riêng lẻ, mà là chúng có nói chung một ngôn ngữ hay không: khi dịch vụ tài khoản gọi dịch vụ hạn mức, cấu trúc request và response mà bên gọi kỳ vọng có khớp với cái bên bị gọi cung cấp không. Kiểm thử hợp đồng (契約テスト) sinh ra để trả lời chính câu hỏi đó, và oracle của nó là tính tương thích hợp đồng.
Hãy đặt oracle lên đầu tiên như mọi bài trong bộ này. Oracle của kiểm thử hợp đồng không phải 'dịch vụ trả về 200' mà là một bất biến mạnh hơn: mọi kỳ vọng mà consumer (bên tiêu thụ) khai báo trong hợp đồng phải được provider (bên cung cấp) thỏa mãn — không thiếu trường bắt buộc, không đổi kiểu dữ liệu, không xóa trường mà consumer đang dùng. Đây là consumer-driven contract: chính bên tiêu thụ định nghĩa nó cần gì, và provider phải chứng minh mình đáp ứng được. Khi hợp đồng bị phá vỡ, ta muốn phát hiện ở CI trước khi triển khai, chứ không phải khi giao dịch của khách hàng thất bại lúc nửa đêm.
2. Vì sao CDC (consumer-driven) thắng kiểm thử E2E giòn
Trước khi có kiểm thử hợp đồng, cách phổ biến để bắt lỗi lệch giao diện là dựng một môi trường tích hợp đầy đủ và chạy E2E. Cách này có ba điểm yếu chí tử ở quy mô ngân hàng: chậm (phải khởi động hàng chục dịch vụ), giòn (một dịch vụ hỏng làm cả bộ E2E đỏ, khó biết nguyên nhân), và tốn kém (duy trì dữ liệu, môi trường, thời gian chạy). Quan trọng hơn, E2E chỉ chạy được khi tất cả dịch vụ đã sẵn sàng — quá muộn để bắt lỗi hợp đồng lúc đang phát triển. Kiểm thử hợp đồng đảo ngược: mỗi cặp consumer-provider được kiểm riêng, nhanh, cô lập.
- E2E: chậm, giòn, đắt, phát hiện muộn — chỉ chạy khi mọi dịch vụ sẵn sàng.
- CDC: nhanh, cô lập, rẻ, phát hiện sớm — kiểm từng cặp consumer-provider.
- CDC không cần provider thật lúc test consumer (dùng stub từ hợp đồng), và không cần consumer thật lúc verify provider (phát lại tương tác).
Cơ chế then chốt là: khi kiểm thử consumer, ta chạy nó chống lại một mock được sinh từ hợp đồng — nếu consumer gọi đúng như đã khai báo, tương tác được ghi thành một 'pact'. Khi verify provider, công cụ phát lại chính những tương tác đó vào provider thật và kiểm tra provider trả về đúng như hợp đồng mô tả. Hai phía không cần chạy đồng thời; chúng gặp nhau qua tài liệu hợp đồng, được lưu ở một broker trung tâm. Nhờ vậy hai đội phát hành độc lập mà vẫn có một oracle chung ràng buộc họ.
3. Cấu trúc một hợp đồng Pact & oracle tương thích
Một hợp đồng Pact là tài liệu JSON mô tả các tương tác mà consumer kỳ vọng: với request nào (method, path, headers, body) thì provider phải trả response nào (status, headers, body). Điểm tinh tế và quan trọng nhất là matcher: hợp đồng không nên cố định giá trị cụ thể (ví dụ số dư đúng bằng 15000) mà nên mô tả hình dạng (số dư là một số nguyên, mã tiền tệ là chuỗi ba ký tự). Nếu cố định giá trị, hợp đồng sẽ giòn và đỏ giả mỗi khi dữ liệu thay đổi; nếu mô tả hình dạng, oracle kiểm đúng thứ ta quan tâm: tính tương thích cấu trúc, không phải dữ liệu nhất thời.
// account.limits.pact.spec.ts — consumer test sinh hợp đồng
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, integer, regex } = MatchersV3;
const provider = new PactV3({ consumer: 'account-svc', provider: 'limits-svc' });
provider
.given('customer c-1 has a daily limit')
.uponReceiving('a check-limit request for 15000')
.withRequest({
method: 'POST', path: '/v1/limits/check',
headers: { 'Content-Type': 'application/json' },
body: { customerId: 'c-1', amount: 15000, currency: 'VND' },
})
.willRespondWith({
status: 200,
// MATCHER: mô tả HÌNH DẠNG, không cố định giá trị -> oracle tương thích
body: {
allowed: like(true),
remaining: integer(2350000),
currency: regex('^[A-Z]{3}$', 'VND'),
},
});Chú ý ba matcher: like(true) chấp nhận bất kỳ boolean nào, integer() chấp nhận bất kỳ số nguyên nào, regex() ràng buộc định dạng mã tiền tệ. Điều consumer thực sự phụ thuộc là: có trường 'allowed' kiểu boolean, 'remaining' là số nguyên, 'currency' đúng định dạng ISO. Nếu ngày mai provider đổi 'remaining' từ số nguyên sang chuỗi, hoặc đổi tên trường, hợp đồng sẽ bắt được ngay khi verify. Đây chính là oracle tương thích: ta khẳng định cấu trúc mà bên tiêu thụ dựa vào, không khẳng định một giá trị cụ thể vốn sẽ thay đổi theo dữ liệu thật.
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!