Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

API Contract Testing — OpenAPI & Postman

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là QA tại một công ty fintech ở TP.HCM. Đội backend công bố một tài liệu OpenAPI (Swagger) mô tả API POST /transfers: response trả về field transactionId kiểu string, amount kiểu number, status là một trong ba giá trị pending | success | failed. Frontend và đội mobile dựa hoàn toàn vào tài liệu đó để code. Rồi một ngày, một dev backend "tiện tay" đổi amount từ number sang string (vì lỡ format tiền tệ có dấu phẩy), hoặc thêm giá trị status: "reversed" mà không cập nhật tài liệu. App mobile crash hàng loạt, còn tài liệu vẫn ghi y như cũ. Không có test functional nào của bạn phát hiện ra — vì logic nghiệp vụ vẫn "đúng", chỉ là hình dạng dữ liệu không còn khớp với hợp đồng đã cam kết.

Đó chính xác là khoảng trống mà Contract Testing (kiểm thử hợp đồng) lấp vào. Trong toàn bộ khóa học này, bạn đã học rất nhiều về cách viết assertion (Bài 8), validate JSON Schema (Bài 10), chạy Newman (Bài 13)… Bài 29 này ghép tất cả lại quanh một câu hỏi trung tâm: API thực tế mà server trả về có khớp với đặc tả (spec) mà chúng ta đã cam kết hay không? Đây là kỹ năng phân biệt một API Tester biết bấm nút với một API Tester hiểu kiến trúc — và là bước đệm trực tiếp lên vai trò SDET.

Khái niệm cốt lõi

Contract testing là gì và khác gì với các loại test khác

Hợp đồng (contract) ở đây là bản đặc tả API — thường được viết theo chuẩn OpenAPI Specification (OAS, trước đây gọi là Swagger). Nó mô tả một cách máy-đọc-được: có những endpoint nào, mỗi endpoint nhận request kiểu gì (path, query, header, body), và trả về response kiểu gì (status code, cấu trúc body, kiểu dữ liệu từng field, field nào bắt buộc, enum có những giá trị nào).

Contract testing là quá trình so sánh hành vi thực tế của API với bản hợp đồng đó. Điều quan trọng cần khắc sâu là ranh giới với hai loại test bạn đã quen:

  • Functional test trả lời: "Logic có đúng không?" — chuyển 500.000đ thì số dư có trừ đúng 500.000đ không.
  • Load/Performance test trả lời: "API có nhanh và chịu tải được không?" — 1.000 request đồng thời, p95 dưới 800ms.
  • Contract test trả lời: "API có đúng hình dạng như đã cam kết không?" — field amount có phải kiểu number không, status có nằm trong enum không, field transactionId bắt buộc có bị thiếu không.
Một API có thể pass functional test nhưng fail contract test: logic trừ tiền vẫn đúng, nhưng backend đổi tên field từ transactionId thành txn_id — nghiệp vụ không sai, nhưng hợp đồng bị phá vỡ và client sẽ gãy.

Consumer-driven vs Provider-driven

Trong thế giới contract testing có hai trường phái. Provider-driven (mà Postman + OpenAPI thiên về): provider — tức đội backend — sở hữu spec, và ta kiểm tra provider có tuân thủ spec của chính mình không. Consumer-driven (kiểu Pact): client — người tiêu thụ API — định nghĩa mình cần gì, rồi provider phải thỏa mãn tập kỳ vọng đó.

Postman không phải công cụ consumer-driven chuyên dụng như Pact, nhưng nó cực mạnh ở provider-side contract validation: import spec vào, tạo collection, rồi validate response thực tế so với schema. Đây là điểm ngọt ngào cho đa số team Việt Nam vì bạn tận dụng lại toàn bộ hạ tầng Postman đã có.

Vì sao OpenAPI là "single source of truth"

Sức mạnh của OpenAPI nằm ở chỗ nó vừa là tài liệu cho con người đọc, vừa là dữ liệu cho máy xử lý. Từ một file openapi.yaml, bạn có thể: sinh code client, sinh mock server (Bài 44 với Prism), render tài liệu đẹp, và validate contract. Khi spec là nguồn chân lý duy nhất, mọi lệch pha giữa tài liệu và thực tế đều trở thành một test fail cụ thể chứ không còn là "cảm giác nghi ngờ".

Tình huống thực tế

Ví dụ 1 — Sàn TMĐT và cái field đổi kiểu âm thầm

Một sàn thương mại điện tử tầm trung ở Hà Nội (gọi là "ShopViet") có API GET /products/{id} được đặc tả trong OpenAPI: price là number, inStock là boolean, tags là mảng string. Đội QA import spec vào Postman, tạo bộ contract test chạy trong pipeline Newman mỗi lần deploy staging.

Một sprint nọ, backend refactor và vô tình để ORM serialize inStock thành số 1/0 thay vì true/false. Functional test vẫn xanh vì "còn hàng thì hiển thị" — cả 1true đều truthy trong JavaScript của frontend web. Nhưng contract test của Postman fail ngay: schema khai báo inStock là boolean, response trả number. Nhờ vậy đội bắt được lỗi trước khi app iOS (nơi Swift phân biệt Bool và Int rất nghiêm ngặt) release ra store và crash.

Bài học: contract test bắt được những sai lệch mà functional test "khoan dung" cho qua, đặc biệt là chênh lệch kiểu dữ liệu giữa các nền tảng client khác nhau.

Ví dụ 2 — Fintech và enum status bị mở rộng ngoài hợp đồng

Một ví điện tử (giả định "PayNhanh") có POST /transactions với field status được đặc tả enum: pending | success | failed. Sau khi tích hợp thêm luồng hoàn tiền, backend thêm giá trị refunded nhưng quên cập nhật openapi.yaml. Contract test — được cấu hình để kiểm tra status phải thuộc enum trong spec — lập tức báo đỏ: "giá trị refunded không có trong enum đã khai báo".

Đây không phải là backend làm sai logic, mà là spec và thực tế đã lệch pha. Contract test buộc đội phải ra quyết định có ý thức: hoặc cập nhật hợp đồng (thêm refunded vào spec, thông báo cho các team consumer), hoặc quyết định refunded không nên lộ ra API này. Dù chọn cách nào, quyết định đó giờ được thực hiện chủ động thay vì để client tự phát hiện khi đã gãy.

Bài học: contract test biến "tài liệu bị lỗi thời" — một vấn đề âm thầm, khó thấy — thành một test fail rõ ràng, ép việc đồng bộ hóa xảy ra.

Ví dụ 3 — Startup SaaS và field mới không được khai báo

Một startup SaaS B2B ở Đà Nẵng có GET /users/{id} trả về profile. Backend thêm field mới internalRiskScore vào response cho mục đích nội bộ, nhưng field này chứa dữ liệu nhạy cảm và lẽ ra không được lộ ra API công khai. Spec OpenAPI được cấu hình additionalProperties: false — nghĩa là "response KHÔNG được có field nào ngoài những field đã khai báo".

Contract test fail ngay khi phát hiện field lạ internalRiskScore. Điều này vừa bảo vệ hợp đồng, vừa vô tình trở thành một lớp phòng thủ bảo mật: ngăn dữ liệu nhạy cảm rò rỉ qua response mà không ai để ý.

Bài học: cấu hình additionalProperties: false trong schema giúp contract test bắt cả field thừa chứ không chỉ field thiếu — hữu ích cho cả tính đúng đắn lẫn an toàn dữ liệu.

Hướng dẫn từng bước

Dưới đây là quy trình thực tế để dựng contract testing với OpenAPI trong Postman.

Bước 1 — Import OpenAPI spec vào Postman

Trong Postman, chọn Import → chọn file openapi.yaml hoặc openapi.json (hoặc dán URL của spec). Postman nhận diện đây là OpenAPI và hỏi bạn muốn generate gì. Chọn Generate collection — Postman sẽ tự tạo một collection với đầy đủ các request tương ứng từng endpoint, kèm ví dụ request/response nếu spec có mô tả.

Lưu ý phiên bản: Postman hỗ trợ tốt OpenAPI 3.0 và 3.1. Nếu bạn còn dùng Swagger 2.0 cũ, cân nhắc convert lên 3.x trước để tránh những chỗ dịch không chuẩn.

Bước 2 — Trích xuất schema từ spec để làm cơ sở validate

Trái tim của contract test là so sánh response thực tế với schema đã khai báo trong spec. Cách phổ biến nhất trong Postman là dùng thư viện ajv (đã có sẵn trong sandbox — xem lại Bài 9 và Bài 10) để validate response body theo JSON Schema lấy từ phần components/schemas của OpenAPI.

Trong tab Tests của request, viết như sau (ví dụ cho endpoint trả về một Product):

const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true, strict: false });

// Schema này copy/tham chiếu từ components.schemas.Product trong openapi.yaml const productSchema = { type: 'object', required: ['id', 'price', 'inStock', 'tags'], additionalProperties: false, properties: { id: { type: 'string' }, price: { type: 'number' }, inStock: { type: 'boolean' }, tags: { type: 'array', items: { type: 'string' } }, status: { type: 'string', enum: ['pending', 'success', 'failed'] } } };

pm.test('Response khớp hợp đồng OpenAPI (Product schema)', function () { const validate = ajv.compile(productSchema); const data = pm.response.json(); const valid = validate(data); if (!valid) { console.log('Contract vi phạm:', validate.errors); } pm.expect(valid, JSON.stringify(validate.errors)).to.be.true; });

Điểm mấu chốt: required bắt field thiếu, additionalProperties: false bắt field thừa, enum bắt giá trị ngoài hợp đồng, và type bắt sai kiểu dữ liệu. Bốn thứ này bao phủ phần lớn vi phạm contract.

Bước 3 — Đừng copy schema thủ công: tự động hóa

Copy tay schema vào từng request rất dễ lệch pha với spec gốc. Có hai hướng chuyên nghiệp hơn:

  • Dereference spec một lần rồi lưu toàn bộ components.schemas vào một collection variable (hoặc environment variable), sau đó trong test chỉ cần lấy schema theo tên: const schema = JSON.parse(pm.collectionVariables.get('schemas')).Product;. Bạn cập nhật biến này mỗi khi spec đổi (có thể tự động bằng script CI).
  • Dùng công cụ chuyên dụng ngoài Postman như Portman (open-source) — nó đọc thẳng openapi.yamltự sinh một Postman collection đầy đủ contract test cho mọi endpoint. Đây là lựa chọn rất được ưa chuộng vì loại bỏ hoàn toàn việc copy tay và luôn đồng bộ với spec.

Bước 4 — Chạy trong CI/CD với Newman

Contract test chỉ thực sự có giá trị khi chạy tự động mỗi lần deploy. Xuất collection ra, rồi trong pipeline (GitHub Actions, GitLab CI — Bài 16, 17) chạy:

newman run contract-tests.postman_collection.json \
  --environment staging.postman_environment.json \
  --reporters cli,junit \
  --reporter-junit-export contract-report.xml

Nếu bất kỳ contract test nào fail, Newman trả exit code khác 0, pipeline đỏ, và deploy bị chặn. Đây là cách bạn biến "hợp đồng" từ lời hứa suông thành ràng buộc được thực thi máy móc.

Bước 5 — Đưa contract test vào đúng thời điểm

Đặt contract test ở staging sau khi build, chạy trước functional test. Lý do: nếu hình dạng dữ liệu đã sai thì functional test cũng vô nghĩa. Ngoài ra, hãy chạy contract test cả trên mock server sinh từ spec (Bài 19, 44) để đảm bảo mock và spec cũng khớp nhau — tránh trường hợp frontend code theo mock nhưng mock lại lệch với backend thật.

Lỗi thường gặp & mẹo

Nhầm contract test với functional test. Đừng nhét assertion nghiệp vụ (số dư đúng, tính toán đúng) vào bộ contract test. Giữ contract test chỉ tập trung vào hình dạng: kiểu, field bắt buộc, enum, cấu trúc. Trộn lẫn khiến test khó bảo trì và khó biết fail vì lý do gì.

Quên additionalProperties: false. Mặc định JSON Schema cho phép field thừa. Nếu bạn muốn contract test bắt được field lạ (như ví dụ 3), phải khai báo tường minh additionalProperties: false. Ngược lại, ở một số API có thể chấp nhận thêm field, thì để mặc định — hãy quyết định có ý thức cho từng schema.

Copy schema thủ công rồi để nó lỗi thời. Đây là cái bẫy lớn nhất. Schema copy tay vào Postman nhanh chóng lệch với openapi.yaml gốc, và bạn kết thúc với contract test sai contract. Ưu tiên tự động sinh (Portman) hoặc nạp schema từ biến được cập nhật bởi CI.

Bỏ qua field nullable. Trong OpenAPI 3.0, nullable: true cho phép field mang giá trị null. Nếu bạn dịch sang JSON Schema mà quên, test sẽ fail oan khi backend trả null hợp lệ. Với OpenAPI 3.1, dùng type: ['string', 'null'] theo chuẩn JSON Schema mới.

Chỉ validate response mà quên request. Hợp đồng hai chiều: request bạn gửi cũng phải đúng spec. Với một QA nghiêm túc, hãy có cả test kiểm tra API từ chối đúng cách khi request sai hợp đồng (ví dụ gửi thiếu field bắt buộc thì phải trả 400, không phải 500).

Mẹo — versioning hợp đồng. Lưu openapi.yaml trong git cùng repo. Mỗi thay đổi spec là một pull request, review được, có lịch sử. Khi contract test fail, bạn nhìn git diff của spec là biết ai đổi gì, khi nào.

Mẹo — thông điệp lỗi rõ ràng. Truyền validate.errors vào message của pm.expect như ví dụ Bước 2. Khi test fail trong Newman, bạn thấy ngay "field amount phải là number nhưng nhận string" thay vì chỉ "assertion failed".

Bài tập thực hành

  • Import và sinh collection. Lấy một file OpenAPI mẫu (ví dụ Petstore của Swagger, hoặc tự viết một spec nhỏ 3 endpoint cho một API sản phẩm). Import vào Postman, generate collection, và kiểm tra các request đã được tạo đúng.
  • Viết contract test đầu tiên. Với endpoint GET /products/{id}, viết test dùng ajv validate response theo schema Product. Bật additionalProperties: falserequired. Cố tình sửa mock response để thiếu một field bắt buộc, xác nhận test fail đúng như mong đợi.
  • Bắt lỗi enum và kiểu dữ liệu. Thêm field status với enum vào schema. Giả lập một response trả status: "reversed" (ngoài enum) và một response trả price kiểu string. Xác nhận cả hai đều bị contract test bắt, và đọc kỹ validate.errors để hiểu thông điệp.
  • Tự động hóa với CI. Export collection, viết một workflow GitHub Actions chạy newman run với reporter JUnit. Cấu hình để pipeline fail khi contract test đỏ. (Kết nối lại kiến thức Bài 16.)
  • Nâng cao — thử Portman. Cài Portman, trỏ vào openapi.yaml của bạn, để nó tự sinh collection contract test, rồi so sánh với bộ test bạn viết tay ở bài 2–3. Ghi lại điểm khác biệt về độ bao phủ.

Tóm tắt

Contract testing trả lời câu hỏi mà functional test và performance test không đụng tới: API thực tế có khớp với hợp đồng OpenAPI đã cam kết không? Nó bắt những sai lệch âm thầm — đổi kiểu dữ liệu, thiếu field bắt buộc, giá trị ngoài enum, field thừa lộ dữ liệu nhạy cảm — những thứ khiến client gãy mà logic nghiệp vụ vẫn "đúng".

Với Postman, quy trình là: import OpenAPI spec → generate collection → validate response theo JSON Schema (dùng ajv, với required, enum, type, additionalProperties: false) → tự động hóa qua Newman trong CI/CD → và ưu tiên tự sinh test (Portman) thay vì copy schema thủ công để tránh lỗi thời. Xem OpenAPI là nguồn chân lý duy nhất, lưu nó trong git, và mỗi lệch pha giữa spec với thực tế sẽ trở thành một test fail cụ thể, buộc cả đội đồng bộ hóa một cách chủ động thay vì để khách hàng phát hiện khi đã quá muộn. Đây chính là kỹ năng đưa bạn từ vai trò "chạy test" lên vai trò "gác cổng kiến trúc API".

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi