DEV Community

Chiến lược kiểm thử API: Hướng dẫn thực tế để có API đáng tin cậy

Chiến lược kiểm thử API: Hướng dẫn thực tế để có API đáng tin cậy

Hầu hết lỗi API không đến từ những tình huống quá đặc biệt. Chúng thường là trường bị thiếu, mã trạng thái sai, timeout khi tải cao, hoặc thay đổi phá vỡ hợp đồng được deploy vì không ai kiểm tra. Kiểm thử ad-hoc đôi khi bắt được vài lỗi như vậy. Một chiến lược kiểm thử API giúp bạn bắt chúng một cách có chủ đích.

Chiến lược kiểm thử API thực sự có nghĩa là gì

Một chiến lược kiểm thử API là kế hoạch xác định bạn kiểm thử cái gì, ở lớp nào và khi nào trong chu trình delivery. Nó cho biết kiểm tra nào chạy ở mỗi commit, kiểm tra nào chạy hàng đêm, và kiểm tra nào bắt buộc trước release. Mục tiêu là đạt độ phủ cao nhất với chi phí bảo trì thấp nhất.

Trước khi viết assertion, hãy trả lời bốn câu hỏi sau.

1. Bạn kiểm thử cái gì?

Ưu tiên endpoint và luồng có rủi ro hoặc giá trị kinh doanh cao. Ví dụ:

  • API thanh toán cần độ phủ cao hơn endpoint health-check
  • Endpoint cập nhật quyền người dùng cần kiểm thử bảo mật kỹ hơn endpoint đọc cấu hình public
  • Luồng tạo đơn hàng cần kiểm thử tích hợp nhiều hơn endpoint trả về metadata

Đừng chọn endpoint chỉ vì nó dễ gọi. Hãy xếp hạng theo:

  • Lưu lượng truy cập
  • Mức độ ảnh hưởng khi lỗi
  • Dữ liệu nhạy cảm
  • Số lượng service phụ thuộc
  • Tần suất thay đổi

2. Kiểm thử ở lớp nào?

Không phải mọi kiểm tra đều nên là end-to-end test. Một số lỗi có thể bắt bằng một request đơn lẻ. Một số lỗi khác cần kiểm tra nhiều service cùng lúc. Nếu bạn đưa tất cả vào lớp cao nhất, test suite sẽ chậm, dễ flaky và khó debug.

3. Khi nào test chạy?

Chia test theo tốc độ và mục đích:

  • Test nhanh: chạy ở mỗi push hoặc pull request
  • Test chậm: chạy hàng đêm hoặc trước release
  • Test tải và bảo mật: chạy theo lịch hoặc trước các mốc quan trọng

Nếu trộn tất cả vào cùng một pipeline, bạn sẽ gặp một trong hai vấn đề: feedback quá chậm hoặc độ phủ quá mỏng.

4. Điều gì được coi là pass?

Một test chỉ kiểm tra 200 OK gần như không đủ. Hãy xác định rõ:

  • Status code mong đợi
  • Response schema
  • Giá trị field quan trọng
  • Error message
  • Response time
  • Side effect, ví dụ record được tạo hoặc trạng thái được cập nhật

Ví dụ assertion cho endpoint lấy user:

expect(response.status).toBe(200)
expect(response.body.id).toBe("42")
expect(response.body.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
expect(response.body).toMatchSchema(UserSchema)

Khi bạn trả lời được bốn câu hỏi này cho API của mình, bạn đã có nền tảng của một chiến lược kiểm thử.

Tại sao chiến lược tốt hơn kiểm thử ad-hoc

Kiểm thử ad-hoc nghĩa là gửi request, nhìn nhanh response, rồi tiếp tục. Cách này hữu ích khi demo hoặc debug nhanh, nhưng không đủ cho hệ thống thật.

Ad-hoc không lặp lại được

Nếu bạn kiểm tra thủ công, người khác khó chạy lại chính xác cùng một bước. Regression bug vì thế dễ quay lại. Một test tự động đã lưu sẽ chạy cùng một cách mỗi lần.

Ad-hoc thiên về happy path

Khi test bằng tay, bạn thường gửi dữ liệu hợp lệ vì muốn xác nhận endpoint hoạt động. Bạn ít khi thử:

  • Payload sai format
  • Token hết hạn
  • Thiếu field bắt buộc
  • Số âm
  • Danh sách 10.000 item
  • Quyền truy cập sai tenant

Nhưng đây lại là các tình huống hay gây lỗi trong production.

Ad-hoc không mở rộng

Một service có 40 endpoint và 5 environment tương đương 200 lượt kiểm tra thủ công cho mỗi release. Không đội nào duy trì được việc này lâu dài. Chiến lược kiểm thử biến chi phí thiết lập ban đầu thành độ phủ lặp lại: viết test một lần, chạy trên mọi thay đổi.

Kim tự tháp kiểm thử API

Kim tự tháp kiểm thử giúp quyết định nên viết bao nhiêu test ở mỗi lớp.

                    /\
                   /  \
   End-to-end     /    \
   / workflow    /------\
   tests (ít,    /        \
   chậm, giá trị /          \
   cao)         /------------\
                /Integration  \
   / contract  /--------------\
   tests (vừa  /                \
   phải, tốc độ/                  \
   trung bình) /--------------------\
               /Unit / single-request\
              / tests (nhiều, nhanh,  \
             / rẻ)                     \
            /____________________________\

Lớp dưới cùng: kiểm tra request đơn lẻ

Mỗi test gọi một endpoint và kiểm chứng response. Ưu điểm: nhanh; rẻ để viết; dễ debug; phù hợp chạy ở mỗi commit.

Ví dụ:

GET /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>

Assertion: status là 200; body khớp schema User; id bằng 42; email hợp lệ.

Phần lớn test API nên nằm ở lớp này.

Lớp giữa: kiểm thử tích hợp và hợp đồng

Lớp này xác minh:

  • Nhiều service giao tiếp đúng với nhau
  • Consumer và provider đồng ý về request/response schema
  • Flow nhiều bước trả về kết quả đúng

Ví dụ: Tạo đơn hàng. Gọi service thanh toán. Kiểm tra trạng thái đơn hàng được cập nhật.

Các test này chậm hơn request đơn lẻ, nhưng bắt được lỗi mà unit/API-level test không thấy.

Lớp trên cùng: workflow end-to-end

Đây là các test chạy toàn bộ hành trình người dùng: Tạo user. Đăng nhập. Tạo đơn hàng. Thanh toán. Kiểm tra trạng thái.

E2E test cho mức độ tự tin cao nhất nhưng tốn chi phí bảo trì cao nhất. Chỉ dùng cho các luồng critical.

Sai lầm phổ biến là đảo ngược kim tự tháp: quá nhiều E2E test chậm, quá ít test nhanh. Nếu một lỗi có thể bắt ở lớp thấp hơn, hãy đưa test xuống lớp thấp hơn.

Các loại kiểm thử và khi nào áp dụng

Một chiến lược hoàn chỉnh dùng nhiều loại test, mỗi loại bắt một nhóm lỗi khác nhau.

Kiểm thử chức năng

Kiểm thử chức năng xác minh endpoint hoạt động đúng theo đặc tả. Nên kiểm tra:

  • Status code
  • Response schema
  • Field bắt buộc
  • Business rule
  • Side effect nếu có

Đây là loại test đầu tiên nên tự động hóa cho endpoint mới.

Ví dụ request:

GET /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer <token>

Checklist assertion:

  • [ ] Status là 200
  • [ ] Body khớp schema User
  • [ ] id bằng 42
  • [ ] email là chuỗi email hợp lệ
  • [ ] Không trả về field nội bộ như passwordHash

Kiểm thử tích hợp

Kiểm thử tích hợp xác minh các endpoint, database, queue, payment provider hoặc service hạ nguồn hoạt động đúng cùng nhau. Một functional test có thể pass với mocked dependency nhưng fail khi kết nối dependency thật. Integration test giúp bắt khoảng trống đó.

Ví dụ flow:

  1. POST /orders
  2. POST /payments
  3. GET /orders/{id}

Kỳ vọng: order được tạo; payment thành công; trạng thái order chuyển sang paid.

Kiểm thử hồi quy

Kiểm thử hồi quy chạy lại test suite hiện có sau mỗi thay đổi để đảm bảo hành vi cũ không bị hỏng.

Bạn không cần viết một nhóm test hoàn toàn mới cho regression. Bạn dùng lại:

  • Functional tests
  • Negative tests
  • Integration tests
  • Contract tests

Sau đó chạy chúng:

  • Ở mỗi pull request
  • Trước release
  • Theo lịch nếu hệ thống thay đổi thường xuyên

Kiểm thử hợp đồng

Kiểm thử hợp đồng xác minh provider và consumer đồng ý về hình dạng request/response. Nó bắt các thay đổi gây lỗi như:

  • Đổi tên field
  • Đổi kiểu dữ liệu
  • Xóa field
  • Xóa endpoint
  • Thay đổi status code
  • Thay đổi format error response

Nếu API của bạn được nhóm khác hoặc khách hàng bên ngoài sử dụng, contract testing là bắt buộc.

Kiểm thử tải và hiệu năng

Kiểm thử tải đo API dưới lưu lượng đồng thời. Các metric cần theo dõi:

  • p95/p99 response time
  • Error rate
  • Throughput
  • CPU/memory
  • Điểm bắt đầu suy giảm
  • Timeout hoặc queue backlog

Không nên chạy load test ở mỗi commit. Hãy chạy:

  • Trước launch
  • Trước chiến dịch có traffic lớn
  • Định kỳ hàng tuần hoặc hàng tháng
  • Sau thay đổi lớn về database, cache hoặc infrastructure

Kiểm thử bảo mật

Kiểm thử bảo mật xác minh API từ chối đúng các request không hợp lệ hoặc không được phép. Tối thiểu cần kiểm tra:

  • Request không có token trả về 401
  • Token hết hạn trả về 401
  • Token hợp lệ nhưng thiếu quyền trả về 403
  • User không đọc được dữ liệu của user khác
  • Input có injection payload không gây lỗi hoặc lộ dữ liệu
  • Dữ liệu nhạy cảm không xuất hiện trong response
Loại kiểm thử Phát hiện Chạy khi
Chức năng Status, schema, field value sai Mỗi commit
Tích hợp Luồng service-to-service bị lỗi Mỗi commit hoặc hàng đêm
Hồi quy Hành vi cũ bị hỏng Mỗi commit và trước release
Hợp đồng Thay đổi phá vỡ interface Mỗi commit trên provider
Tải Chậm hoặc lỗi dưới traffic Trước launch và theo lịch
Bảo mật Auth, injection, lộ dữ liệu Trước release và theo lịch

Các trường hợp tích cực, tiêu cực và biên

Với mỗi endpoint, hãy viết test cho ba nhóm input.

Trường hợp tích cực

Gửi input hợp lệ và mong đợi thành công. Ví dụ tạo user:

POST /api/users HTTP/1.1
Content-Type: application/json

{
  "name": "Nguyen Van A",
  "email": "a@example.com"
}

Kỳ vọng: status 201; response có id; email đúng với input; record được tạo trong hệ thống.

Trường hợp tiêu cực

Gửi input không hợp lệ và mong đợi lỗi rõ ràng. Ví dụ thiếu field bắt buộc:

POST /api/users HTTP/1.1
Content-Type: application/json

{
  "name": "Nguyen Van A"
}

Kỳ vọng: status 400; error message chỉ rõ email bị thiếu; không tạo record.

Một vài negative case nên có:

  • Thiếu token
  • Token hết hạn
  • Sai quyền
  • Body sai schema
  • Field sai kiểu
  • ID không tồn tại
  • Truy cập dữ liệu không thuộc user hiện tại

Trường hợp biên

Gửi input ở ranh giới hợp lệ. Ví dụ:

  • Chuỗi dài đúng bằng giới hạn tối đa
  • Danh sách rỗng
  • Danh sách có số lượng item tối đa
  • Số 0
  • Số âm nếu không hợp lệ
  • Unicode name
  • Timestamp ở ranh giới timezone hoặc daylight saving time

Quy tắc thực tế: với mỗi positive test, hãy viết ít nhất một negative test.

Ví dụ endpoint tạo đơn hàng:

POST /api/orders HTTP/1.1
Content-Type: application/json

{
  "customerId": "c_123",
  "quantity": -5
}

Kỳ vọng: status 400; body chứa lỗi validation cho quantity; không tạo order. Nếu request này trả về 201 hoặc 500, bạn đã tìm thấy lỗi mà happy-path test không bao giờ bắt được.

Dữ liệu kiểm thử và môi trường

Test chỉ đáng tin nếu dữ liệu và môi trường ổn định.

Sử dụng dữ liệu kiểm thử chuyên dụng

Không nên test dựa trên production record hoặc một row tình cờ tồn tại trong database. Thay vào đó:

  • Tạo dữ liệu trong bước setup
  • Seed dataset cố định trước khi chạy
  • Dọn dữ liệu sau test
  • Dùng test user/test tenant riêng

Làm test độc lập

Mỗi test nên tự setup trạng thái cần thiết. Không nên phụ thuộc vào thứ tự chạy như: Test B chỉ pass nếu Test A đã tạo user trước đó.

Nếu cần truyền dữ liệu giữa các bước trong cùng một scenario, hãy làm rõ ràng:

  1. POST /users
  2. Lưu response.body.id vào biến userId
  3. GET /users/{{userId}}

Tránh dùng state toàn cục chia sẻ giữa nhiều test.

Cô lập môi trường

Tách riêng: local; CI; staging; production. Mỗi môi trường nên có:

  • Base URL riêng
  • Token riêng
  • Database hoặc dataset riêng
  • Cấu hình timeout riêng
  • Secret riêng

Đừng hard-code host hoặc token trong test. Ví dụ dùng biến:

GET {{baseUrl}}/api/users/{{userId}}
Authorization: Bearer {{accessToken}}

Cùng một test có thể chạy trên local, staging hoặc CI bằng cách đổi environment variable.

Shift Left: kiểm thử sớm hơn, không chỉ nhiều hơn

Shift left nghĩa là đưa kiểm thử về sớm hơn trong vòng đời phát triển. Một lỗi schema phát hiện ở giai đoạn thiết kế có thể sửa trong vài phút. Cùng lỗi đó ở production có thể trở thành incident.

1. Thiết kế hợp đồng trước

Xác định request và response schema trước khi build endpoint. Ví dụ:

User:
  type: object
  required:
    - id
    - email
  properties:
    id:
      type: string
    email:
      type: string
      format: email

Từ contract này, bạn có thể:

  • Tạo mock server
  • Viết test trước
  • Cho frontend tích hợp sớm
  • Phát hiện breaking change trước khi deploy

2. Test với mock khi backend chưa hoàn thiện

Frontend hoặc service consumer không cần chờ backend hoàn tất. Nếu mock được tạo từ schema, consumer có thể kiểm tra:

  • Request đúng format
  • Response parsing
  • Error handling
  • UI state cho loading/error/success

3. Chạy test nhanh ở mỗi commit

Functional test và contract test nhanh nên nằm trong developer feedback loop. Đừng đợi batch hàng đêm mới biết endpoint vừa bị hỏng. Càng phát hiện sớm, chi phí sửa càng thấp.

Tự động hóa kiểm thử trong CI

Chiến lược chỉ hiệu quả nếu test chạy tự động. Mục tiêu của pipeline:

  • Chạy test ở mỗi push hoặc pull request
  • Fail build nếu assertion fail
  • Xuất report dễ đọc
  • Cho phép debug nhanh

Một pipeline CI cho API test thường có ba nhóm:

  • Mỗi push: chạy functional test và contract test nhanh
  • Hàng đêm hoặc trước release: chạy integration, E2E, load và security test
  • Luôn luôn: xuất report, ví dụ JUnit XML, để CI hiển thị số pass/fail

Ví dụ GitHub Actions tối thiểu:

name: api-tests
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: Install dependencies
        run: npm ci
      - name: Run API tests
        run: npm test

Pattern chung: checkout code; cài runtime; cài dependencies; inject secrets bằng CI variables; chạy test; fail build nếu exit code khác 0; publish report.

Ví dụ dùng biến môi trường:

- name: Run API tests
  env:
    API_BASE_URL: ${{ secrets.API_BASE_URL }}
    API_TOKEN: ${{ secrets.API_TOKEN }}
  run: npm test

Apidog phù hợp với chiến lược như thế nào

Chiến lược ở trên không phụ thuộc công cụ. Bạn có thể dùng nhiều công cụ riêng cho thiết kế API, mock, test và tài liệu. Vấn đề là các artifact dễ bị lệch nhau:

  • Spec thay đổi nhưng test chưa đổi
  • Mock không khớp contract mới
  • Tài liệu khác với behavior thật
  • CI chạy test cũ

Apidog gom các phần này vào một nơi:

  • Thiết kế hợp đồng API
  • Viết test scenario dựa trên hợp đồng
  • Tạo mock từ cùng schema
  • Xuất bản tài liệu
  • Chạy test đã lưu trong CI

Vì test và mock dùng cùng contract, contract testing và shift-left testing trở thành một phần tự nhiên của workflow.

Chạy test Apidog trong CI bằng CLI

Apidog CLI chạy scenario và test suite đã lưu mà không cần giao diện UI. CLI là một package Node, nên có thể dùng trong hầu hết CI runner có Node.

Cài đặt:

npm install -g apidog-cli

Chạy một scenario hoặc test suite:

apidog run \
  --access-token "$APIDOG_ACCESS_TOKEN" \
  -t <scenarioOrSuiteId> \
  -e <environmentId> \
  -r cli,html,junit

Ý nghĩa các tham số:

  • --access-token: token xác thực. Lưu trong CI secrets
  • -t: ID của scenario, folder hoặc test suite cần chạy
  • -e: ID environment

Comments

No comments yet. Start the discussion.