API testing là mảnh đất mà AI phát huy sức mạnh rõ nhất trong automation, vì API có tính chất structured (OpenAPI spec, Postman collection, JSON schema...) — nguồn input rất "sạch" để LLM suy luận ra test case một cách có hệ thống, không mơ hồ như UI. Nhưng cũng chính vì dễ sinh ra số lượng lớn test nhanh, QA Engineer càng cần kỹ năng prompt đúng cách để tránh nhận về hàng trăm test case trùng lặp, hời hợt hoặc sai schema. Chủ đề này tập trung vào cách khai thác OpenAPI/Postman để sinh test có chiều sâu, cách sinh test theo ma trận positive/negative/boundary, và cách dùng AI hỗ trợ contract testing (kiểm thử hợp đồng) giữa consumer và provider.
Làm sao sinh API Test Case từ OpenAPI Spec hoặc Postman Collection bằng AI?
OpenAPI spec (trước đây gọi là Swagger) và Postman collection là hai nguồn input phổ biến nhất để AI sinh test case API, vì cả hai đều mô tả rõ endpoint, method, request/response schema — AI không cần đoán mò cấu trúc dữ liệu như khi làm việc với UI.
Đưa OpenAPI Spec Cho AI
Cách hiệu quả nhất không phải là dán toàn bộ file OpenAPI (có thể rất dài, tốn context window — vùng ngữ cảnh mà LLM có thể xử lý trong một lần), mà là trích riêng phần path liên quan đến flow bạn muốn test:
/api/orders:
post:
summary: Tạo đơn hàng mới
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Đơn hàng được tạo thành công
content:
application/json:
schema:
$ref: '#/components/schemas/OrderResponse'
'400':
description: Dữ liệu không hợp lệ
'401':
description: Chưa xác thực
Cấu Trúc Prompt Sinh Test Từ OpenAPI
Một prompt sinh test hiệu quả từ OpenAPI spec nên nêu rõ: framework test (ví dụ Playwright APIRequestContext, Supertest, hoặc pytest + requests), format assertion mong muốn, và giới hạn phạm vi (chỉ 1-2 endpoint mỗi lần, tránh sinh tràn lan):
Dựa vào OpenAPI spec cho endpoint POST /api/orders ở trên, viết API test
bằng Playwright Test (dùng request fixture có sẵn của Playwright, TypeScript).
Yêu cầu:
- Test case cho response 201: verify status code, verify response body
match đúng structure của schema OrderResponse (id, status, totalAmount, items)
- Test case cho response 400: gửi body thiếu field "items", verify status 400
và response có field "errors" là array
- Test case cho response 401: gửi request không kèm Authorization header
- Base URL lấy từ process.env.API_BASE_URL
- Đặt file tại tests/api/orders/create-order.spec.ts
Output mẫu:
// tests/api/orders/create-order.spec.ts
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.API_BASE_URL;
test.describe('POST /api/orders', () => {
test('tạo đơn hàng thành công trả về 201 và đúng schema', async ({ request }) => {
const response = await request.post(`${BASE_URL}/api/orders`, {
headers: { Authorization: `Bearer ${process.env.TEST_TOKEN}` },
data: {
items: [{ sku: 'SHOE-001', quantity: 1 }],
shippingAddress: { name: 'Nguyen Van A', phone: '0901234567', address: '12 Nguyen Hue' },
paymentMethod: 'COD',
},
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body).toMatchObject({
id: expect.any(String),
status: expect.any(String),
totalAmount: expect.any(Number),
items: expect.any(Array),
});
});
test('thiếu field items trả về 400 kèm errors', async ({ request }) => {
const response = await request.post(`${BASE_URL}/api/orders`, {
headers: { Authorization: `Bearer ${process.env.TEST_TOKEN}` },
data: { shippingAddress: {}, paymentMethod: 'COD' },
});
expect(response.status()).toBe(400);
const body = await response.json();
expect(Array.isArray(body.errors)).toBe(true);
});
test('không có Authorization header trả về 401', async ({ request }) => {
const response = await request.post(`${BASE_URL}/api/orders`, {
data: { items: [{ sku: 'SHOE-001', quantity: 1 }] },
});
expect(response.status()).toBe(401);
});
});
Sinh Test Từ Postman Collection
Với Postman collection (file .json export), bạn có thể trích riêng request cần test và đưa cho AI kèm theo môi trường (environment variables) đang dùng:
Đây là 1 request trong Postman collection của tôi (dạng JSON export):
[dán JSON của request "Get Order By Id"]
Chuyển request này thành 1 test suite bằng pytest + requests, giữ nguyên
các biến môi trường Postman ({{baseUrl}}, {{authToken}}) bằng cách map sang
os.environ. Thêm test cho case order không tồn tại (404) và order thuộc
user khác (403 - authorization check).
Trích Xuất Component Schema Để Assertion Tốt Hơn
Một lỗi thường gặp khi AI sinh test từ OpenAPI là chỉ assert status code mà bỏ qua việc verify schema response. Để tránh điều này, hãy luôn trích riêng phần components.schemas liên quan và yêu cầu AI dùng nó làm nguồn assertion:
Đây là schema OrderResponse đầy đủ:
OrderResponse:
type: object
required: [id, status, totalAmount, items]
properties:
id: { type: string }
status: { type: string, enum: [pending, confirmed, cancelled] }
totalAmount: { type: number, minimum: 0 }
items:
type: array
items: { $ref: '#/components/schemas/OrderItem' }
Viết thêm assertion kiểm tra "status" chỉ nhận 1 trong 3 giá trị enum trên,
và "totalAmount" phải >= 0. Có thể dùng Zod hoặc Ajv để validate schema
thay vì check field thủ công nếu project đã có sẵn thư viện đó.
Mẹo: Khi làm việc với OpenAPI spec lớn (hàng trăm endpoint), đừng bao giờ dán cả file vào 1 prompt — hãy dùng công cụ như swagger-cli hoặc script nhỏ để trích riêng path + schema liên quan trước, sau đó mới đưa vào prompt. Việc này không chỉ tiết kiệm context window mà còn giúp AI tập trung, tránh lẫn schema của endpoint không liên quan vào test.
Làm sao sinh Test Case Positive, Negative, Boundary API trong một Prompt?
Một trong những lợi thế lớn nhất của AI trong API testing là khả năng sinh nhanh một "ma trận" test case bao trùm positive (đường đi đúng), negative (đầu vào sai) và boundary (giá trị biên) chỉ trong một lần prompt — điều mà viết tay sẽ tốn rất nhiều thời gian lặp lại cấu trúc.
Prompt Ma Trận Test (Test Matrix)
Cấu trúc prompt hiệu quả là liệt kê rõ 3 nhóm case bạn muốn, với ví dụ cụ thể cho từng nhóm, tránh để AI tự quyết định phạm vi:
Với endpoint POST /api/products (tạo sản phẩm mới), field "price" là
number, required, minimum 0, maximum 999999999.
Sinh test case theo ma trận sau (mỗi case là 1 test riêng, dùng
test.each hoặc parametrize nếu framework hỗ trợ):
Positive:
- price = 100000 (giá trị hợp lệ thông thường)
- price = 0 (biên dưới hợp lệ)
- price = 999999999 (biên trên hợp lệ)
Negative:
- price = -1 (dưới biên) → expect 400
- price = 1000000000 (vượt biên) → expect 400
- price = "abc" (sai type) → expect 400
- price = null (thiếu required) → expect 400
- price = không có field price → expect 400
Boundary bổ sung:
- price = 0.01 (số thập phân nhỏ nhất nếu hệ thống cho phép)
- price sinh ngẫu nhiên gần biên bằng property-based testing nếu
framework hỗ trợ (ví dụ fast-check cho TypeScript)
Viết bằng Playwright Test + TypeScript, dùng test.describe.parametrize
pattern (hoặc for loop tạo test.() động).
Output mẫu dùng vòng lặp để tránh lặp code:
import { test, expect } from '@playwright/test';
const boundaryCases = [
{ price: 100000, expectStatus: 201, label: 'giá trị hợp lệ thông thường' },
{ price: 0, expectStatus: 201, label: 'biên dưới hợp lệ' },
{ price: 999999999, expectStatus: 201, label: 'biên trên hợp lệ' },
{ price: -1, expectStatus: 400, label: 'âm - dưới biên' },
{ price: 1000000000, expectStatus: 400, label: 'vượt biên trên' },
{ price: 'abc', expectStatus: 400, label: 'sai type' },
{ price: null, expectStatus: 400, label: 'null' },
];
for (const c of boundaryCases) {
test(`POST /api/products - price: ${c.label}`, async ({ request }) => {
const response = await request.post('/api/products', {
data: { name: 'Test Product', price: c.price },
});
expect(response.status()).toBe(c.expectStatus);
});
}
test('POST /api/products - thiếu field price', async ({ request }) => {
const response = await request.post('/api/products', {
data: { name: 'Test Product' },
});
expect(response.status()).toBe(400);
});
Sinh Test Ma Trận Theo HTTP Method
Bên cạnh ma trận theo giá trị field, bạn cũng có thể yêu cầu AI sinh ma trận theo HTTP method cho cùng 1 resource — kỹ thuật rất hữu ích để phát hiện các method chưa được implement đúng hoặc thiếu chặn quyền:
Với resource /api/orders/{id}, sinh test case kiểm tra hành vi của mỗi
HTTP method:
- GET: trả về order nếu tồn tại và thuộc user hiện tại, 404 nếu không tồn tại
- PUT: chỉ cho phép khi order.status = "pending", trả 409 nếu status khác
- DELETE: chỉ admin mới có quyền, trả 403 với user thường
- PATCH, OPTIONS: nếu chưa implement, method phải trả 405 Method Not Allowed
(không được trả 500)
Mẹo: Khi sinh test ma trận, luôn yêu cầu AI dùng cấu trúc dữ liệu (array of test cases + loop, hoặc test.each) thay vì viết N test case riêng lẻ dài dòng — không chỉ code ngắn gọn hơn, mà khi cần thêm case mới bạn (hoặc AI ở lượt sau) chỉ cần thêm 1 dòng vào array, giảm hẳn rủi ro copy-paste sai khi maintain.
Làm sao dùng AI để sinh Consumer-Driven Contract Test?
Contract testing (kiểm thử hợp đồng) — điển hình là mô hình consumer-driven với Pact — giải quyết bài toán: hai team (frontend/consumer và backend/provider) phát triển độc lập nhưng cần đảm bảo API không "vỡ hợp đồng" giữa hai bên mà không cần chạy full E2E integration. AI có thể hỗ trợ sinh cả phần consumer test (định nghĩa pact) và phần provider verification.
Sinh Pact Definition Cho Consumer
Ở phía consumer, bạn mô tả kỳ vọng của mình về response từ provider, AI sẽ sinh test dùng Pact library để capture interaction đó thành file pact (JSON):
Tôi đang viết consumer contract test bằng Pact (JS - @pact-foundation/pact)
cho frontend gọi API GET /api/orders/{id} từ order-service.
Consumer: order-frontend
Provider: order-service
Kỳ vọng: khi gọi GET /api/orders/ORD-001 với header Authorization hợp lệ,
provider trả về 200 với body:
{
"id": "ORD-001",
"status": "confirmed",
"totalAmount": 590000
}
Viết consumer test dùng Pact, verify interaction, và cấu hình publish
pact file lên Pact Broker sau khi test pass.
// tests/contracts/order-service.pact.spec.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
const { like, string } = MatchersV3;
const provider = new PactV3({
consumer: 'order-frontend',
provider: 'order-service',
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('GET /api/orders/{id}', () => {
it('trả về order khi tồn tại', () => {
provider
.given('order ORD-001 tồn tại và thuộc user hiện tại')
.uponReceiving('a request for order ORD-001')
.withRequest({
method: 'GET',
path: '/api/orders/ORD-001',
headers: { Authorization: like('Bearer token123') },
})
.willRespondWith({
status: 200,
body: {
id: string('ORD-001'),
status: string('confirmed'),
totalAmount: like(590000),
},
});
return provider.executeTest(async (mockServer) => {
const res = await fetch(`${mockServer.url}/api/orders/ORD-001`, {
headers: { Authorization: 'Bearer token123' },
});
const body = await res.json();
expect(body.id).toBe('ORD-001');
});
});
});
Sinh Provider Pact Verification Test
Ở phía provider (order-service), test cần verify rằng service thực tế đáp ứng đúng pact đã được consumer publish:
Viết provider verification test cho order-service (Node.js/Express)
dùng @pact-foundation/pact Verifier, đọc pact file từ Pact Broker
(URL lấy từ env PACT_BROKER_URL).
Provider state "order ORD-001 tồn tại và thuộc user hiện tại" cần được
setup bằng cách seed database test với order đó trước khi verify.
// tests/contracts/provider-verify.spec.ts
import { Verifier } from '@pact-foundation/pact';
import { seedOrder, cleanupOrders } from '../helpers/db-seed';
describe('Pact Verification - order-service', () => {
it('validates the expectations of order-frontend', () => {
return new Verifier({
provider: 'order-service',
providerBaseUrl: 'http://localhost:4000',
pactBrokerUrl: process.env.PACT_BROKER_URL,
publishVerificationResult: true,
providerVersion: process.env.GIT_COMMIT,
stateHandlers: {
'order ORD-001 tồn tại và thuộc user hiện tại': async () => {
await seedOrder({ id: 'ORD-001', status: 'confirmed', totalAmount: 590000 });
},
},
afterEach: cleanupOrders,
}).verifyProvider();
});
});
Mẹo: Khi prompt AI sinh contract test, luôn nêu rõ tên chính xác của "provider state" (given(...)) và bắt AI dùng đúng string đó ở cả hai phía consumer và provider — Pact match interaction theo state name dạng string, sai một ký tự là verification fail âm thầm mà không có lỗi rõ ràng, rất khó debug nếu không để ý ngay từ khi generate.
Làm sao sinh Test Case cho Authentication, Pagination và Error-Handling bằng AI?
Ba nhóm hành vi này (xác thực, phân trang, xử lý lỗi) xuất hiện lặp lại ở gần như mọi API, nên rất đáng để chuẩn hóa thành prompt template tái sử dụng được cho toàn bộ hệ thống endpoint, thay vì viết lại từ đầu cho mỗi resource.
Sinh Test Authentication
Sinh test case authentication áp dụng chung cho các endpoint cần auth
(GET /api/orders, GET /api/profile, POST /api/orders):
- Không có Authorization header → 401
- Token sai format (không phải Bearer) → 401
- Token hợp lệ nhưng đã expired → 401 với message "Token expired"
- Token hợp lệ nhưng đã bị revoke (logout) → 401
- Token hợp lệ, đúng user, nhưng thiếu quyền cho resource cụ thể → 403
- Token hợp lệ, đủ quyền → 200/201 theo đúng endpoint
Viết dưới dạng 1 shared test helper function `runAuthTestSuite(endpoint, method)`
để tái sử dụng cho nhiều endpoint khác nhau, dùng Playwright request fixture.
Sinh Test Pagination
Endpoint GET /api/orders hỗ trợ pagination qua query param page, limit,
trả về response có metadata: { data: [], page, limit, totalItems, totalPages }.
Sinh test case:
- page=1&limit=10 với 25 record có sẵn → trả về đúng 10 item, totalPages=3
- page=3&limit=10 (trang cuối) → trả về đúng 5 item còn lại
- page=999 (vượt quá totalPages) → trả về data rỗng, không lỗi 500
- limit=0 hoặc limit âm → 400
- không truyền page/limit → dùng default (page=1, limit=20 theo doc)
- limit vượt max cho phép (ví dụ limit=1000, max=100) → tự động cap về 100,
không trả lỗi
Test Cấu Trúc Error Response
Chuẩn error response của hệ thống là:
{ "errors": [{ "field": string, "message": string, "code": string }] }
Sinh test đảm bảo MỌI response lỗi (400, 404, 409, 422) đều tuân theo
đúng structure này, không có endpoint nào trả lỗi dạng string thô hoặc
thiếu field "code". Viết 1 test riêng dùng schema validation (Ajv/Zod)
áp cho tất cả response lỗi thu thập được từ danh sách endpoint sau:
[liệt kê endpoint cần check]
Kết Hợp Authentication, Pagination Và Error-Handling Trong Một Suite
Sau khi có các template riêng, bạn có thể yêu cầu AI ghép lại thành một suite hoàn chỉnh cho một resource cụ thể — đây là lúc giá trị của việc chuẩn hóa từng phần nhỏ phát huy tối đa:
Ghép 3 bộ test trên (auth, pagination, error structure) thành 1 file
tests/api/orders/orders-list.spec.ts hoàn chỉnh cho GET /api/orders,
tổ chức theo test.describe lồng nhau: "Authentication", "Pagination",
"Error Response Structure". Đảm bảo mỗi describe block dùng chung
beforeAll để seed 25 order test data, và afterAll để cleanup.
Mẹo: Đóng gói 3 template này (auth/pagination/error-structure) thành các file prompt riêng lưu trong repo (ví dụ docs/prompts/api-test-templates.md) để cả team dùng lại nhất quán — tránh tình trạng mỗi người tự viết lại prompt hơi khác nhau, dẫn đến test case ở các module khác nhau kiểm tra không đồng bộ (module A check token revoke, module B lại quên).