Lập trình · 19/09/2026

Kiểm thử contract API với OpenAPI: Chặn lỗi tích hợp ngay trong CI

API vẫn trả HTTP 200, nhưng frontend đột nhiên lỗi vì một field biến mất, đổi kiểu hoặc trở thành null. Unit test của backend có thể vẫn xanh, tài liệu Swagger vẫn mở được, nhưng hợp đồng mà consumer đang dựa vào đã bị phá vỡ.

Kiểm thử contract API với OpenAPI: Chặn lỗi tích hợp ngay trong CI

API vẫn trả HTTP 200, nhưng frontend đột nhiên lỗi vì một field biến mất, đổi kiểu hoặc trở thành null. Unit test của backend có thể vẫn xanh, tài liệu Swagger vẫn mở được, nhưng hợp đồng mà consumer đang dựa vào đã bị phá vỡ.

Kiểm thử contract dựa trên OpenAPI biến mô tả API thành một artefact có thể kiểm tra tự động. Thay vì chỉ dùng file YAML để dựng trang tài liệu, đội ngũ dùng nó để xác minh cấu trúc request, response, status code và content type trong mỗi pull request.

Kiểm thử contract dựa trên schema là gì?

OpenAPI mô tả giao diện HTTP theo cách độc lập ngôn ngữ: endpoint, method, tham số, authentication, request body và response. Một công cụ có thể đọc mô tả này để sinh tài liệu, client, mock hoặc test mà không cần hiểu mã nguồn của service.

Trong bài này, “contract testing” có nghĩa là kiểm tra implementation có tuân thủ OpenAPI schema hay không. Nó không hoàn toàn giống consumer-driven contract testing, nơi từng consumer công bố kỳ vọng riêng và provider xác minh các kỳ vọng đó. Hai cách có thể bổ sung nhau:

Lớp kiểm traPhát hiện tốtKhông thay thế
Schema lintOpenAPI sai cú pháp, reference hỏng, quy ước thiếuHành vi của server đang chạy
Schema conformanceStatus, header và JSON thực tế lệch contractLogic nghiệp vụ sâu
Consumer-driven contractProvider làm hỏng kỳ vọng thật của consumerKhám phá input biên toàn API
Integration/E2ELuồng nghiệp vụ qua nhiều thành phầnPhản hồi nhanh, định vị lỗi hẹp
Contract test không chứng minh API đúng toàn bộ nghiệp vụ. Nó chứng minh giao diện mà hai bên đã thỏa thuận chưa bị thay đổi ngoài ý muốn.

1. Chọn một nguồn sự thật

Contract chỉ có giá trị khi đội ngũ biết artefact nào là nguồn sự thật. Có hai hướng phổ biến:

  • Design-first: sửa openapi.yaml, review thay đổi, sau đó backend và client cùng triển khai.
  • Code-first: sinh OpenAPI từ annotation, route hoặc type trong mã nguồn, rồi kiểm tra file sinh ra trong CI.

Cả hai đều dùng được, nhưng tránh duy trì thủ công hai bản độc lập. Nếu spec trong repository nói một kiểu còn endpoint runtime nói kiểu khác, trang tài liệu đẹp chỉ tạo cảm giác an toàn giả.

Nên lưu contract cùng source control, review diff như code và gắn phiên bản API nghiệp vụ trong info.version. Trường openapi là phiên bản đặc tả mà tooling dùng để diễn giải tài liệu; nó không thay cho phiên bản sản phẩm của API.

2. Viết schema đủ chặt để có thể kiểm thử

Một schema toàn type: object và cho phép mọi thuộc tính gần như không bảo vệ được consumer. Hãy mô tả những điều thực sự tạo thành hợp đồng:

  • Field bắt buộc và field có thể vắng mặt.
  • Kiểu dữ liệu, định dạng, enum, giới hạn độ dài và miền giá trị.
  • Khả năng nhận null tách biệt với tính bắt buộc.
  • Content type, status code thành công và các lỗi có cấu trúc.
  • Pagination, envelope và quy tắc tương thích khi thêm field.
openapi: 3.1.0
info:
  title: Order API
  version: 1.4.0
paths:
  /orders/{orderId}:
    get:
      operationId: getOrder
      parameters:
        - in: path
          name: orderId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Order found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          description: Order not found
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
components:
  schemas:
    Order:
      type: object
      required: [id, status, total]
      properties:
        id: { type: string, format: uuid }
        status:
          type: string
          enum: [pending, paid, cancelled]
        total:
          type: integer
          minimum: 0
          description: Amount in the smallest currency unit

OpenAPI 3.1 căn chỉnh Schema Object với JSON Schema Draft 2020-12. Dù vậy, hãy chọn phiên bản mà toàn bộ linter, generator và test runner trong pipeline hỗ trợ, thay vì chỉ đổi số phiên bản rồi hy vọng tooling hiểu giống nhau.

3. Lint contract trước khi khởi động ứng dụng

Lớp nhanh nhất chỉ đọc file tĩnh. Nó nên thất bại khi YAML/JSON không hợp lệ, $ref không giải được, path parameter thiếu khai báo hoặc operation không có response cần thiết.

Ngoài lỗi đặc tả, đội ngũ nên có ruleset riêng cho những quy ước quan trọng:

  • Mọi operation có operationId duy nhất.
  • Các endpoint được bảo vệ khai báo security scheme.
  • Response lỗi dùng cùng một envelope, ví dụ RFC 9457 Problem Details.
  • Endpoint danh sách có pagination và giới hạn tối đa.
  • Không đưa dữ liệu nhạy cảm vào example.

Lint chạy trong vài giây và nên là job đầu tiên. Nếu contract chưa tự hợp lệ, chưa có lý do để build container hay chạy test tốn thời gian.

4. Xác minh response thực tế bằng schema

Sau khi ứng dụng khởi động trong môi trường test, gọi endpoint bằng dữ liệu đã kiểm soát và validate đồng thời:

  1. Status code có được khai báo cho operation hay không.
  2. Content-Type có khớp media type hay không.
  3. Body có thỏa schema của đúng status và media type hay không.
  4. Header bắt buộc có mặt và đúng dạng hay không.

Một assertion chỉ kiểm tra response.status === 200 sẽ bỏ sót nhiều lỗi. Ví dụ server trả total: "125000" thay vì integer, hoặc trả HTML error page với status 200. Validator contract phải chọn schema theo response thực tế, không ép mọi kết quả vào happy path.

Kiểm tra cả response lỗi. API thường được test kỹ ở 200 nhưng lại để 401, 403, 404 và 422 trả bốn cấu trúc khác nhau, khiến client phải đoán.

5. Sinh test biên từ OpenAPI

Test viết tay thường dùng vài ví dụ “đẹp”. Property-based testing đọc constraint để sinh nhiều input hợp lệ và không hợp lệ: chuỗi rỗng, số sát biên, enum ngoài danh sách, Unicode, field thiếu hoặc object lồng nhau.

Schemathesis là một công cụ có thể chạy trực tiếp với schema tĩnh và base URL của ứng dụng test:

uvx schemathesis run ./openapi.yaml \
  --url http://127.0.0.1:8080

Runner có thể phát hiện server error, status không được mô tả và response không khớp schema, đồng thời đưa ra request có thể tái hiện. Chạy ban đầu với endpoint đọc hoặc một test environment có thể reset; không fuzz thẳng production.

Với endpoint có authentication, truyền credential test có quyền tối thiểu. Secret phải lấy từ secret store của CI, không ghi vào OpenAPI, command đã commit hoặc artefact test.

6. Đừng để test phá dữ liệu

Generation-based test có thể gọi POST, PATCH và DELETE nhiều lần. Trước khi bật toàn bộ schema, cần dựng ranh giới an toàn:

  • Dùng database/container tách biệt và reset được.
  • Chặn kết nối tới payment, email và dịch vụ production.
  • Thay external side effect bằng sandbox hoặc fake có kiểm soát.
  • Lọc operation nguy hiểm cho đến khi fixture và cleanup hoàn chỉnh.
  • Gắn tenant test riêng, quota nhỏ và thời gian sống ngắn.

Nếu API tạo chuỗi resource phụ thuộc, khai báo OpenAPI Links hoặc chuẩn bị fixture rõ ràng. Một test tạo order cần biết customer/product hợp lệ; gửi UUID ngẫu nhiên rồi nhận 404 hàng nghìn lần không tạo ra coverage có ý nghĩa.

7. Thiết kế pipeline theo tầng

Một pipeline cân bằng tốc độ và độ tin cậy có thể chia thành bốn tầng:

  1. Static: parse, lint, resolve reference và kiểm tra policy.
  2. Diff: so contract mới với nhánh chính để phát hiện breaking change.
  3. Example tests: chạy các ca nghiệp vụ xác định, ổn định và dễ đọc.
  4. Generated tests: khám phá input biên trên service vừa build.
contract-lint
      |
contract-breaking-change
      |
build-and-start-test-service
      |
example-tests + generated-schema-tests

Lint và diff nên fail nhanh. Test sinh tự động có thể giới hạn số case trong pull request và chạy sâu hơn theo lịch. Lưu JUnit/report cùng seed hoặc curl command tái hiện để lỗi ngẫu nhiên vẫn debug được.

8. Nhận diện breaking change đúng bối cảnh

Một số thay đổi thường phá consumer:

  • Xóa endpoint, status response, field hoặc enum value mà client cần.
  • Đổi kiểu dữ liệu, format hoặc semantic của field.
  • Biến field tùy chọn thành bắt buộc trong request.
  • Siết min/max, pattern hoặc giới hạn độ dài của input.
  • Thêm yêu cầu authentication mới.

“Thêm field response luôn tương thích” chỉ đúng khi consumer được thiết kế để bỏ qua field lạ. “Thêm enum value” cũng có thể phá client dùng switch exhaustive. Vì vậy diff tool đưa ra tín hiệu, còn policy tương thích phải phản ánh SDK và consumer thực tế của tổ chức.

Nếu thay đổi có chủ đích, ưu tiên tiến hóa tương thích: thêm field mới trước, giữ field cũ trong giai đoạn deprecation, đo usage, rồi mới xóa ở version lớn hoặc sau cửa sổ thông báo.

9. Tránh drift giữa spec và implementation

Drift xảy ra khi code đổi mà contract không đổi, hoặc contract được thiết kế nhưng implementation chưa theo kịp. Có ba điểm kiểm soát hiệu quả:

  • Pull request sửa route/DTO phải kèm diff OpenAPI tương ứng.
  • CI chạy schema conformance trên artefact vừa build, không dùng server dùng chung cũ.
  • Runtime hoặc smoke test định kỳ lấy schema đang public và kiểm tra endpoint quan trọng.

Nếu code-first, hãy generate spec trong CI rồi fail khi working tree xuất hiện diff chưa commit. Nếu design-first, mock giúp frontend làm việc sớm, nhưng provider vẫn phải vượt qua conformance test trước khi merge.

10. Những điều schema không nói hết

OpenAPI mô tả hình dạng giao tiếp tốt hơn ý nghĩa nghiệp vụ. Schema có thể xác nhận total là integer không âm, nhưng không biết tổng tiền có bằng các dòng hàng sau giảm giá hay không. Nó cũng không tự chứng minh:

  • Authorization đúng theo owner/role.
  • Transaction, idempotency và concurrency an toàn.
  • Pagination không bỏ sót hoặc trùng record.
  • SLA, rate limit và latency đạt mục tiêu.
  • Workflow nhiều bước chuyển trạng thái hợp lệ.

Giữ unit, integration, security và performance test. Contract test là lớp bảo vệ biên giao tiếp, không phải chiếc ô thay thế toàn bộ chiến lược chất lượng.

Checklist áp dụng

  • Có một nguồn sự thật OpenAPI được version control.
  • Schema khai báo required, nullability, enum, format và lỗi đủ rõ.
  • Lint và resolve $ref chạy trước build nặng.
  • Response runtime được validate theo status và content type thực.
  • Breaking-change check so sánh với contract đang phát hành.
  • Generated tests chỉ chạy trong môi trường cô lập, reset được.
  • Credential test có quyền tối thiểu và không nằm trong artefact.
  • Failure lưu request/seed có thể tái hiện nhưng đã che dữ liệu nhạy cảm.
  • Test nghiệp vụ, authorization và hiệu năng vẫn được duy trì riêng.

Kết luận

OpenAPI hữu ích nhất khi không dừng ở trang tài liệu. Khi contract được lint, so diff và đối chiếu với response thật trong CI, sai lệch giữa backend, frontend và SDK được phát hiện trước khi đến production. Hãy bắt đầu từ một endpoint quan trọng, làm schema đủ chặt, thêm conformance test rồi mở rộng dần sang generation-based testing. Một pipeline nhỏ nhưng chạy trên mọi thay đổi đáng tin cậy hơn một bộ tài liệu lớn chỉ được cập nhật trước ngày phát hành.

Tài liệu tham khảo

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.