Nếu frontend testing là nơi bạn "nhìn thấy" lỗi bằng mắt, thì backend và API testing là nơi lỗi ẩn sâu trong dữ liệu, trong hợp đồng giữa các service, và trong những giả định không ai viết ra thành văn bản. Một QA Engineer làm việc với backend lâu năm sẽ biết: phần khó nhất không phải là viết assertion, mà là chuẩn bị trạng thái dữ liệu đúng để test có ý nghĩa, và đảm bảo hợp đồng (contract) giữa các service không âm thầm bị phá vỡ khi một team khác deploy.
AI không thay bạn ra quyết định "test case nào quan trọng" — nhưng nó có thể rút ngắn 70-80% thời gian bạn tốn vào việc sinh fixture, viết schema validator, dựng mock server, hay soạn contract test. Bài này đi sâu vào bốn mảng cốt lõi của backend/API testing hiện đại: quản lý state database, schema & data integrity, contract testing theo hướng consumer-driven, và mock/simulation cho service dependency. Tất cả đều có prompt mẫu bạn có thể copy-paste và chỉnh theo stack của mình.
Làm thế nào để dùng AI sinh script setup và teardown trạng thái database?
Bất kỳ ai từng maintain một bộ integration test lâu năm đều thấm câu này: test logic hiếm khi là nguyên nhân test flaky, fixture (dữ liệu mẫu dùng để setup test) mới là kẻ phá hoại thầm lặng. AI là công cụ rất mạnh để giải quyết bài toán này, vì sinh fixture là việc tốn công lặp lại và có pattern rõ ràng — chính xác loại việc LLM làm tốt.
Thách thức cốt lõi: fixtures rot
"Fixture rot" (fixture bị mục nát theo thời gian) là hiện tượng dữ liệu test dần dần không còn phản ánh đúng schema hoặc business rule thực tế của hệ thống. Nó xảy ra theo vài cách rất quen:
- Schema đổi, fixture không đổi theo: Team backend thêm cột
NOT NULLmới vào bảngorders, nhưng fileorders.fixture.jsonviết từ 8 tháng trước vẫn thiếu field đó. Test vẫn chạy được (vì DB test có thể chưa migrate, hoặc fixture insert qua ORM tự điền default), nhưng nó không còn phản ánh dữ liệu thật trong production. - Business rule thay đổi nhưng fixture "hardcode" giá trị cũ: Ví dụ trạng thái đơn hàng trước đây có 3 state (
pending,paid,shipped), giờ thêmpartially_refunded. Fixture cũ không bao giờ test được nhánh mới. - Fixture được copy-paste và chỉnh tay qua nhiều PR: Không ai còn nhớ tại sao
user_id = 42lại córole = 'legacy_admin'. Xoá thì sợ vỡ test khác, giữ thì không hiểu ý nghĩa. - Fixture phụ thuộc thứ tự (order-dependent): Test A tạo dữ liệu, test B giả định dữ liệu đó tồn tại. Chạy riêng lẻ thì pass, chạy song song (parallel) thì fail ngẫu nhiên.
Cách AI giúp ở đây không phải là "viết lại toàn bộ fixture một lần cho xong", mà là biến việc tái sinh fixture từ schema hiện tại thành việc rẻ đến mức bạn làm nó thường xuyên, mỗi khi schema đổi — giống như bạn chạy lại npm install sau khi pull code mới.
Mẹo: Đừng yêu cầu AI "tạo dữ liệu test giả" một cách chung chung — luôn feed nó schema thật (file migration, DDL, hoặc ORM model) làm nguồn sự thật (source of truth). Fixture sinh ra từ trí nhớ của AI về "một bảng users điển hình" sẽ luôn lệch pha với schema thật của bạn.
Sinh seed data từ schema definitions
Cách làm hiệu quả nhất: đưa AI file migration hoặc DDL (Data Definition Language) thật của bạn, để nó tự suy ra kiểu dữ liệu, constraint, foreign key, và sinh seed data tôn trọng toàn bộ các ràng buộc đó — bao gồm cả những ràng buộc bạn quên nhắc.
Ví dụ prompt với schema PostgreSQL:
Đây là schema migration hiện tại của bảng orders và order_items (Postgres):
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES customers(id),
status VARCHAR(20) NOT NULL CHECK (status IN ('pending','paid','shipped','partially_refunded','cancelled')),
total_cents INTEGER NOT NULL CHECK (total_cents >= 0),
currency CHAR(3) NOT NULL DEFAULT 'VND',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
shipped_at TIMESTAMPTZ,
CHECK (shipped_at IS NULL OR status IN ('shipped','partially_refunded'))
);
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price_cents INTEGER NOT NULL CHECK (unit_price_cents >= 0)
);
Hãy sinh cho tôi:
1. 5 bộ seed data cho orders, đủ để cover từng giá trị status hợp lệ, kể cả các trường hợp biên
của check constraint (ví dụ: order đã shipped phải có shipped_at, order pending phải có
shipped_at = NULL).
2. Với mỗi order, sinh 2-3 order_items tương ứng, tổng unit_price_cents * quantity phải khớp
với total_cents của order cha (tôi cần tính đúng, không cần AI tự bịa).
3. Xuất dưới dạng file SQL INSERT, có comment giải thích lý do chọn từng bộ giá trị.
4. Thêm 1 bộ dữ liệu "invalid" minh họa test case sẽ FAIL nếu constraint bị insert sai
(dùng để test rằng DB constraint thực sự chặn được, không phải chỉ chặn ở tầng application).
Điểm mấu chốt của prompt này: yêu cầu AI giải thích lý do chọn từng giá trị, và yêu cầu nó tự kiểm tra tính nhất quán (tổng order_items khớp total_cents). Nếu không yêu cầu rõ, AI rất dễ sinh dữ liệu "trông hợp lý" nhưng vi phạm chính constraint mà bạn vừa cho nó đọc.
Với hệ thống dùng ORM (Prisma, TypeORM, Sequelize, ActiveRecord...), bạn có thể feed file model/schema thay cho DDL thô — AI đọc TypeScript/Ruby class cũng suy luận tốt không kém.
Sinh fixture helper theo ngôn ngữ (language-native)
Fixture rời rạc dạng file JSON/SQL tĩnh dễ rot. Cách bền hơn là sinh ra fixture helper function — hàm factory theo đúng ngôn ngữ/framework bạn dùng, cho phép override từng field khi cần, và luôn đồng bộ với schema vì nó gọi trực tiếp vào model thật.
Prompt ví dụ cho Node.js + Prisma, dùng pattern factory phổ biến (tương tự factory_bot của Ruby hay factory-boy của Python):
Dựa trên Prisma schema sau (model Order, OrderItem, Customer), hãy viết cho tôi
một factory function bằng TypeScript, dùng thư viện @faker-js/faker, theo pattern:
- createOrder(overrides?: Partial<CreateOrderInput>): trả về object có thể insert
trực tiếp qua prisma.order.create(). Mọi field NOT NULL phải có giá trị hợp lệ
mặc định, field optional để undefined.
- createOrderWithItems(itemCount = 2, overrides?): tạo order + n order_items,
đảm bảo total_cents luôn khớp tổng của item.
- Cho phép override bất kỳ field nào qua tham số thứ 2 (deep merge, không phải
shallow overwrite).
- Factory phải fail nhanh (throw lỗi rõ ràng) nếu override truyền vào status
không nằm trong enum hợp lệ — đừng để lỗi này rơi xuống tầng DB constraint.
Viết kèm 3 test case minh họa cách dùng factory này trong Jest, bao gồm 1 case
override status = 'shipped' và show rằng shippedAt tự động được set.
Fixture helper dạng hàm có 3 lợi ích so với file tĩnh: (1) nó tự khớp schema vì gọi qua model thật, migration đổi thì compiler báo lỗi ngay; (2) mỗi test tự override đúng phần mình cần, giảm coupling giữa test; (3) dễ đọc — người review nhìn createOrder({ status: 'cancelled' }) hiểu ngay chủ đích test, thay vì phải grep file JSON để tìm object nào ứng với case nào.
Mẹo: Khi yêu cầu AI viết factory, luôn chỉ định rõ thư viện data giả bạn dùng (faker, chance.js, Fishery...) và pattern override bạn muốn (deep merge hay shallow). Nếu không, AI sẽ chọn ngẫu nhiên một convention không khớp codebase hiện tại, và bạn tốn công sửa lại nhiều hơn là viết từ đầu.
Sinh script teardown
Teardown bị đánh giá thấp hơn setup, nhưng lại là nguồn gốc chính của flaky test khi chạy song song hoặc chạy trên CI có state tồn dư giữa các lần chạy. AI khá tốt trong việc phân tích schema (đặc biệt là các quan hệ khóa ngoại) để sinh ra thứ tự xoá đúng, tránh vi phạm ràng buộc foreign key.
Dựa trên schema Postgres tôi đã đưa (orders, order_items, customers, products,
payments — payments tham chiếu orders, order_items tham chiếu orders và products),
hãy viết cho tôi:
1. Một script teardown TypeScript dùng Prisma Client, xoá toàn bộ dữ liệu test
theo đúng thứ tự tránh lỗi foreign key constraint (bảng con trước, bảng cha sau).
2. Một biến thể chỉ xoá dữ liệu được tạo trong một "test run" cụ thể — giả sử mọi
bảng liên quan đều có cột created_by_test_run_id (uuid), hãy xoá theo điều kiện
đó thay vì TRUNCATE toàn bộ bảng.
3. Giải thích khi nào nên dùng TRUNCATE ... CASCADE thay vì xoá tuần tự, và rủi ro
của việc TRUNCATE khi test chạy song song trên schema chia sẻ.
Có ba chiến lược teardown phổ biến mà AI có thể giúp bạn cài đặt tuỳ theo tình huống:
- Transaction rollback: mở transaction ở đầu mỗi test, rollback ở cuối. Nhanh nhất, nhưng không dùng được nếu code test cần commit thật (ví dụ test qua nhiều connection, hoặc test async worker đọc DB bằng connection khác).
- Truncate theo bảng: đơn giản, dễ hiểu, nhưng chậm hơn transaction và không an toàn khi nhiều test chạy song song trên cùng schema.
- Xoá có điều kiện theo tag/run-id: mỗi record test tạo ra được gắn
test_run_id, teardown chỉ xoá theo tag đó. Đây là cách duy nhất an toàn cho môi trường test chạy song song hoặc môi trường staging chia sẻ.
Mẹo: Luôn yêu cầu AI viết kèm bước "dry-run" — một câu lệnh SELECT COUNT(*) xác nhận số record sẽ bị ảnh hưởng trước khi chạy DELETE/TRUNCATE thật, đặc biệt nếu script này có khả năng được chạy nhầm trên môi trường staging.
Quản lý test state cho multi-schema và multi-tenant
Hệ thống SaaS đa tenant (multi-tenant) hoặc hệ thống có nhiều schema (ví dụ mỗi service có schema riêng trong cùng một Postgres instance, hoặc kiến trúc schema-per-tenant) đặt ra một lớp phức tạp khác: dữ liệu test của tenant A không được "leak" sang test của tenant B, và setup/teardown phải scale theo số tenant test cần.
Đây là nơi AI giúp nhiều nhất ở việc sinh ra chiến lược, không chỉ script đơn lẻ:
Hệ thống của tôi dùng kiến trúc schema-per-tenant trên Postgres (mỗi tenant có
schema riêng: tenant_001, tenant_002, ...), dùng chung một bộ migration.
Tôi cần một chiến lược test isolation cho integration test chạy trên CI với
4 worker song song (Jest --maxWorkers=4). Hãy đề xuất và viết code cho:
1. Cách tạo schema test tạm thời (ví dụ tenant_test_<worker_id>_<uuid>) trước
mỗi test suite, áp toàn bộ migration vào đó.
2. Cách set search_path hoặc connection string động theo schema đang test.
3. Cách đảm bảo schema tạm bị xoá dọn ngay cả khi test suite fail giữa đường
(dùng afterAll kết hợp với xử lý lỗi, không để leak schema rác trên CI).
4. Cảnh báo nếu tạo/drop schema theo cách này sẽ quá chậm cho CI, đề xuất
phương án thay thế (ví dụ dùng schema template + CREATE SCHEMA ... AS clone,
hoặc pool schema tái sử dụng).
Với hệ multi-tenant dùng row-level isolation (một schema chung, mỗi row có tenant_id), yêu cầu AI khác đi một chút: sinh factory luôn kèm tenant_id ngẫu nhiên riêng cho mỗi test, và sinh assertion helper kiểm tra chắc chắn rằng query không vô tình trả record của tenant khác (kiểm thử row-level security/RLS policy).
Mẹo: Với hệ multi-tenant, luôn dành riêng một nhóm test chuyên trách "tenant isolation" — cố ý viết test cố truy cập chéo tenant để xác nhận nó bị chặn. AI sinh case này rất nhanh nếu bạn mô tả đúng RLS policy hoặc middleware kiểm tra tenant hiện tại.
Làm thế nào để dùng AI cho schema validation và kiểm thử data integrity?
Test logic ứng dụng là một chuyện, nhưng dữ liệu thật trong production không phải lúc nào cũng đi qua đúng đường application code — có migration chạy tay, có script import, có backfill hàng loạt. Data integrity testing (kiểm thử tính toàn vẹn dữ liệu) là lớp phòng thủ đảm bảo dữ liệu luôn nhất quán với các quy tắc mà hệ thống giả định là đúng.
Cần test gì trong data integrity testing
Trước khi nhờ AI sinh test, cần xác định rõ các lớp integrity cần kiểm tra — nếu không, bạn sẽ chỉ nhận về những test constraint hời hợt (kiểu "NOT NULL thì không được null", ai cũng biết):
- Entity integrity: mỗi bảng có primary key hợp lệ, không trùng, không null.
- Referential integrity: mọi foreign key trỏ tới record thật sự tồn tại — đặc biệt quan trọng sau khi chạy migration xoá hàng loạt hoặc archive dữ liệu cũ.
- Domain integrity: giá trị field nằm trong phạm vi/enum hợp lệ (ví dụ
status,currency, email format). - Business/semantic integrity: các quy tắc nghiệp vụ không thể diễn tả bằng constraint SQL đơn giản — ví dụ "tổng
order_itemsphải khớporders.total_cents", "một user không thể có 2 subscription active cùng lúc". - Temporal integrity: các mốc thời gian phải theo đúng thứ tự logic —
shipped_atkhông thể nhỏ hơncreated_at,cancelled_atkhông thể tồn tại cùng lúc vớicompleted_at. - Uniqueness ngữ nghĩa: unique constraint ở tầng DB có thể tồn tại (email unique), nhưng có những unique rule phức tạp hơn cần xử lý ở tầng ứng dụng hoặc partial index (ví dụ "chỉ 1 địa chỉ mặc định cho mỗi user", tức unique trên
(user_id) WHERE is_default = true).
Mẹo: Trước khi prompt AI viết test, hãy tự liệt kê business rule bằng tiếng Việt/tiếng Anh đơn giản trước (dạng bullet), rồi mới đưa cho AI chuyển thành test. AI sinh test rất giỏi khi rule đã rõ ràng, nhưng nó không thể tự "phát hiện" các business rule ẩn mà không ai viết ra — nó chỉ đoán dựa trên schema, và đoán sai thì test sẽ sai theo.
Sinh schema constraint tests
Đây là lớp test cơ bản nhất nhưng vẫn cần làm kỹ: xác nhận rằng constraint bạn định nghĩa trong migration thực sự hoạt động đúng như kỳ vọng — không chỉ tồn tại, mà chặn đúng case cần chặn và không chặn nhầm case hợp lệ.
Đây là đầy đủ CHECK constraint, FOREIGN KEY, UNIQUE constraint trong file
migration 0032_orders_constraints.sql (đã đính kèm). Hãy viết bộ test bằng
pytest + SQLAlchemy (kết nối trực tiếp DB test, không mock) để:
1. Với mỗi CHECK constraint, viết 1 test insert dữ liệu VI PHẠM constraint,
assert rằng nó raise IntegrityError đúng loại, và 1 test insert dữ liệu
NGAY TRÊN biên hợp lệ (ví dụ quantity = 1 nếu constraint là quantity > 0)
để xác nhận biên đó KHÔNG bị chặn nhầm.
2. Với FOREIGN KEY, viết test insert order_item tham chiếu order_id không
tồn tại, xác nhận bị chặn; và test xoá order cha khi có order_item con,
xác nhận đúng hành vi ON DELETE (CASCADE hoặc RESTRICT theo migration).
3. Với UNIQUE constraint composite, viết test 2 record cùng giá trị composite
key bị chặn, nhưng khác nhau ở 1 field trong composite thì không bị chặn.
4. Không dùng mock ORM — test phải chạy trên DB test thật (fixture pytest
connect vào docker-compose Postgres của project) để đảm bảo constraint DB
thật hoạt động, không chỉ validation ở tầng Python.
Điểm quan trọng ở prompt trên: nhấn "test ngay trên biên hợp lệ" — nhiều bộ constraint test chỉ test case vi phạm mà quên test rằng giá trị hợp lệ hợp lệ không bị chặn nhầm (off-by-one trong CHECK constraint là lỗi rất phổ biến, ví dụ viết >= 0 nhưng ý định là > 0).
Sinh data integrity query cho audit production
Ngoài test tự động trong CI, một kỹ năng quan trọng của Senior QA là viết audit query — câu SQL chạy định kỳ (hoặc theo yêu cầu) trên production/staging để phát hiện dữ liệu đã "rot" theo thời gian, thường do bug lịch sử hoặc migration lỗi mà không ai phát hiện ngay.
Dựa trên schema orders/order_items/payments tôi đã cung cấp, hãy viết cho tôi
5 audit query SQL, mỗi query kèm comment giải thích nó phát hiện loại lỗi
integrity nào, và kèm ước tính mức độ nghiêm trọng (critical/warning):
1. Order có total_cents không khớp SUM(order_items.quantity * unit_price_cents).
2. Order có status = 'shipped' nhưng shipped_at IS NULL (hoặc ngược lại).
3. Payment có amount_cents khác order.total_cents nhưng status = 'completed'.
4. Order_items tham chiếu product_id đã bị soft-delete (products.deleted_at
IS NOT NULL) — nghĩa là đơn hàng chứa sản phẩm "không còn tồn tại" theo
logic hiện tại.
5. Customer có nhiều hơn 1 order với cùng idempotency_key — dấu hiệu bug
double-charge.
Với mỗi query, thêm 1 dòng SQL đếm số lượng record vi phạm (COUNT), để tôi
có thể dùng ngay trong dashboard giám sát hoặc alert.
Những query này không phải test tự động chạy trong pipeline CI (dù bạn hoàn toàn có thể đưa một phần vào CI dưới dạng "canary query" chạy trên snapshot production đã ẩn danh) — chúng là công cụ audit định kỳ, giúp QA phát hiện lỗi đã tồn tại âm thầm trong dữ liệu thật, thứ mà unit test không bao giờ chạm tới vì unit test chỉ dùng dữ liệu do chính nó tạo ra.
AI-assisted migration validation
Migration là một trong những hoạt động rủi ro cao nhất với data integrity — nó chạy một lần, thường trên production, và rollback (nếu có) hiếm khi được test kỹ như migration forward. AI hỗ trợ tốt ở việc review migration script để tìm rủi ro trước khi nó chạy thật.
Đây là migration Rails/ActiveRecord tôi sắp chạy trên production (bảng orders
có 40 triệu record):
add_column :orders, :region_code, :string, null: false, default: 'VN'
add_index :orders, :region_code
Hãy review migration này và trả lời:
1. Với 40 triệu record, thêm cột NOT NULL có default trên Postgres phiên bản
13 có gây table lock dài hay không? Nếu có, đề xuất cách viết lại an toàn
hơn (ví dụ add_column không default trước, backfill theo batch, rồi mới
add NOT NULL sau).
2. add_index không dùng CONCURRENTLY có khoá write trên bảng orders không?
Viết lại migration dùng algorithm: :concurrently nếu cần, và giải thích
trade-off (không chạy được trong transaction DDL).
3. Viết test migration bằng cách mô tả: chạy migration trên bản copy schema
với 1000 record giả, đo thời gian, và assert migration chạy xong không
raise lỗi và index được tạo đúng.
4. Viết kịch bản rollback an toàn — remove_column có mất dữ liệu không nếu
phải rollback sau khi cột đã được ghi dữ liệu thật?
Đây là ví dụ AI đóng vai trò "reviewer thứ hai" trước khi migration động tới dữ liệu thật — không thay thế DBA, nhưng bắt được nhiều lỗi phổ biến (table lock, thiếu CONCURRENTLY, thiếu chiến lược backfill theo batch) mà một QA hoặc dev không chuyên sâu về Postgres internals dễ bỏ sót.
Mẹo: Luôn hỏi AI câu "migration này an toàn với bảng có X triệu record không?" thay vì chỉ hỏi "migration này đúng cú pháp không?" — cú pháp gần như luôn đúng, nhưng tác động vận hành (lock, thời gian chạy, rollback) mới là thứ gây sự cố thật ở production.
Sinh API response validation schema
Lớp integrity cuối cùng cần kiểm tra là hợp đồng giữa dữ liệu trong DB và những gì API trả ra cho client — rất nhiều bug "API trả sai dữ liệu" thực chất là do thiếu validation ở tầng serialization. AI sinh JSON Schema hoặc Zod/Pydantic schema từ OpenAPI spec (hoặc ngược lại) rất nhanh và chính xác.
Đây là OpenAPI spec (đính kèm) định nghĩa response 200 của GET /api/v1/orders/{id}.
Hãy sinh cho tôi:
1. Một Zod schema TypeScript tương ứng, validate đầy đủ: type, required fields,
enum cho status, format cho các trường datetime (ISO 8601), và range cho
totalCents (>= 0).
2. Một test Jest gọi API thật (qua supertest) với 3 order id đã seed sẵn (pending,
shipped, cancelled), parse response qua Zod schema trên, và assert
schema.safeParse(response.body).success === true.
3. Một test riêng cố ý gọi order id không tồn tại, assert response 404 khớp
với error schema đã định nghĩa trong OpenAPI spec (không phải mặc định của
framework, ví dụ không phải HTML lỗi Express default).
Việc validate response theo schema (thay vì assert từng field lẻ tẻ bằng tay) giúp test ngắn hơn, dễ maintain hơn, và — quan trọng nhất — tự động phát hiện thêm field mới không có trong spec hoặc thiếu field theo spec mà không cần viết thêm assertion nào, nếu bạn cấu hình schema với chế độ strict (.strict() trong Zod, additionalProperties: false trong JSON Schema).
Mẹo: Bật strict mode/additionalProperties: false khi sinh schema validation cho API, dù ban đầu bạn nghĩ "thêm field thừa không hại gì". Rất nhiều incident thực tế bắt nguồn từ việc API vô tình trả field nội bộ (internal field) ra ngoài — ví dụ trả luôn passwordHash hoặc internalNotes — mà chỉ có schema strict mới bắt được ngay từ vòng test.
Contract testing theo hướng consumer-driven với AI hoạt động ra sao?
Khi hệ thống của bạn có nhiều service giao tiếp qua API (kiến trúc microservices), integration test end-to-end kiểu "dựng toàn bộ hệ thống rồi test" ngày càng đắt và chậm. Consumer-driven contract testing (CDCT) giải quyết vấn đề này bằng cách kiểm tra "hợp đồng" giữa consumer (bên gọi API) và provider (bên cung cấp API) một cách độc lập, không cần dựng cả hệ thống.
Mô hình contract testing
Ý tưởng cốt lõi của CDCT (ví dụ theo mô hình Pact — công cụ phổ biến nhất cho pattern này):
- Consumer viết test mô tả kỳ vọng của mình về API provider (request nào sẽ gửi, response nào mong nhận được). Test này sinh ra một file contract/pact — bản mô tả tương tác đã được thoả thuận.
- Contract được publish lên một broker (Pact Broker hoặc tương đương) — nơi lưu trữ trung tâm mọi contract giữa các service.
- Provider chạy lại các tương tác trong contract đó (provider verification) để xác nhận nó thực sự trả đúng response như consumer kỳ vọng.
- Nếu provider đổi API theo cách phá vỡ hợp đồng, bước verification fail ngay trong CI của provider — trước khi provider được deploy, không phải sau khi consumer gặp lỗi ở production.
Điểm khác biệt quan trọng so với integration test truyền thống: consumer là người định nghĩa hợp đồng, không phải provider. Điều này buộc provider phải chủ động quan tâm "ai đang dùng API của tôi, và họ mong đợi gì" — thay vì chỉ tự test theo hiểu biết chủ quan của chính team provider.
Mẹo: Contract testing không thay thế được integration test hoàn toàn — nó cực tốt cho việc bắt breaking change giữa service, nhưng không kiểm tra được hành vi nghiệp vụ end-to-end (ví dụ luồng đặt hàng có đúng hay không). Hãy dùng contract test để giảm số lượng integration test tốn kém, không phải xoá bỏ hoàn toàn integration test.
Sinh consumer contract với AI
Viết contract test bằng tay khá tốn công vì cú pháp Pact (hoặc Pactflow) có nhiều boilerplate. AI sinh khá tốt nếu bạn cho nó biết rõ consumer thực sự gọi API như thế nào (code thật, không phải mô tả chung).
Tôi là team frontend (consumer), gọi API GET /api/v1/orders/{orderId} của
service "orders-service" (provider). Đây là đoạn code thật tôi dùng để gọi
API này (axios, TypeScript, đính kèm), và đây là response mẫu tôi đang nhận
được từ môi trường staging (JSON đính kèm).
Hãy viết cho tôi consumer contract test bằng Pact JS (@pact-foundation/pact),
gồm:
1. Khai báo interaction: request GET /api/v1/orders/{orderId} với orderId cụ
thể, headers Authorization Bearer token dạng matcher (không hardcode giá
trị token thật).
2. Response 200 với body dùng Pact matchers (like(), eachLike(), term()) cho
từng field — KHÔNG hardcode giá trị cụ thể cho field không quan trọng về
giá trị (ví dụ id dùng uuid matcher, createdAt dùng iso8601DateTime matcher),
nhưng field có ý nghĩa nghiệp vụ (status) phải dùng term() với regex đúng
enum hợp lệ.
3. Một interaction riêng cho case 404 (order không tồn tại).
4. Cấu hình publish contract lên Pact Broker sau khi test pass, dùng
pact-broker publish trong CI (giải thích luôn nên đặt bước này ở đâu
trong pipeline).
Việc dùng matcher thay vì hardcode giá trị là kỹ năng quan trọng nhất khi viết contract — matcher lỏng vừa đủ (like structure, không lock giá trị cụ thể) giúp contract không fail vì lý do vô nghĩa (giá trị createdAt khác nhau mỗi lần chạy), nhưng vẫn chặt đủ (term với regex enum) để bắt breaking change thật.
Sinh provider verification test
Ở phía provider, verification test cần "replay" lại contract đã consumer định nghĩa, chạy trên API thật của provider (không mock), và xác nhận response khớp.
Tôi là team backend, chủ API orders-service. Đây là pact file mà team frontend
đã publish lên Pact Broker (đính kèm JSON). Hãy viết provider verification
test bằng Pact JS Verifier, chạy trên Express app thật của tôi (không mock
Express, chạy app thật trên port test), gồm:
1. Cấu hình Verifier.verifyProvider() trỏ tới pactBrokerUrl, providerBaseUrl
là app Express chạy local trên port ngẫu nhiên trong test.
2. State handler cho mỗi "provider state" khai báo trong pact (ví dụ
"an order with id abc123 exists") — viết hàm setup DB test tương ứng
trước khi verification chạy interaction đó, và teardown sau khi xong.
3. Cấu hình publishVerificationResult: true để kết quả verify được ghi lại
lên Pact Broker, cho phép frontend team biết API của mình có tương thích
hay chưa qua "can-i-deploy" check.
4. Giải thích cách chạy can-i-deploy trong CI của provider TRƯỚC khi deploy,
để chặn deploy nếu phá vỡ contract của bất kỳ consumer nào.
Phần "provider state" là chỗ nhiều team làm sai lúc mới bắt đầu: state handler phải thực sự tạo ra dữ liệu đúng như tên state mô tả (ví dụ dùng lại fixture helper đã sinh ở phần đầu bài này), nếu không verification sẽ pass giả — API trả response đúng cấu trúc nhưng không phải vì dữ liệu thật tồn tại, mà vì handler rỗng hoặc mock sai chỗ.
Phát hiện contract drift với AI
"Contract drift" là khi API provider dần thay đổi theo thời gian (thêm field, đổi kiểu dữ liệu, đổi enum) mà không có consumer nào chủ động cập nhật contract — dẫn tới hợp đồng cũ dần không còn phản ánh đúng API thật, cho tới khi ai đó phát hiện qua một bug ở production.
AI hữu ích ở việc diff giữa contract hiện tại và OpenAPI spec/response thật mới nhất, để tìm chỗ lệch trước khi nó thành incident:
Đây là pact contract hiện tại giữa consumer "web-app" và provider
"orders-service" (JSON đính kèm), và đây là response thật mới nhất tôi
vừa curl từ staging của orders-service cho cùng endpoint.
So sánh 2 nguồn này và chỉ ra:
1. Field nào có trong response thật nhưng KHÔNG có trong contract (nghĩa là
provider đã thêm field mới mà chưa ai cập nhật contract — có thể vô hại,
nhưng cần xác nhận consumer có cần field đó không).
2. Field nào có trong contract nhưng KHÔNG có trong response thật (breaking
change nghiêm trọng — consumer đang mong đợi field mà provider đã xoá
hoặc đổi tên).
3. Field nào có mặt ở cả 2 nhưng kiểu dữ liệu hoặc format khác nhau (ví dụ
contract mong number nhưng response thật trả string).
4. Với mỗi điểm lệch, đề xuất: cần update contract, hay đây là breaking
change cần báo ngay cho team provider trước khi merge.
Nên chạy loại kiểm tra này định kỳ (ví dụ nightly job trong CI) giữa contract trong broker và response thật từ môi trường staging — biến việc phát hiện drift từ "phản ứng sau khi có bug" thành "phát hiện chủ động trước khi bug xảy ra".
Sinh contract test từ OpenAPI spec
Nếu team của bạn đã maintain OpenAPI spec (Swagger) đầy đủ, bạn có thể dùng AI để bootstrap luôn bộ contract test ban đầu từ spec đó, thay vì viết từ đầu — đặc biệt hữu ích khi onboard contract testing vào một service đã tồn tại lâu năm, có sẵn spec nhưng chưa có contract test nào.
Đây là toàn bộ file openapi.yaml của service "payments-service" (đính kèm),
định nghĩa 8 endpoint. Tôi muốn bootstrap contract test cho 3 endpoint quan
trọng nhất mà team "checkout-service" (consumer) đang gọi:
POST /payments, GET /payments/{id}, POST /payments/{id}/refund.
Hãy:
1. Với mỗi endpoint, sinh 1 Pact consumer test TypeScript, dựa 100% vào
request/response schema định nghĩa trong OpenAPI (không tự bịa field
ngoài spec).
2. Với mỗi field có "example" trong OpenAPI spec, dùng chính giá trị example
đó làm giá trị demo trong matcher, để contract dễ đọc và có ý nghĩa nghiệp
vụ rõ ràng thay vì giá trị ngẫu nhiên vô nghĩa.
3. Với field có enum trong spec (ví dụ status: [pending, succeeded, failed]),
dùng term() matcher với regex đúng enum đó — không dùng like() lỏng lẻo
cho field enum.
4. Liệt kê riêng những field trong OpenAPI có đánh dấu "deprecated: true" —
cảnh báo tôi không nên đưa field đó vào contract mới.
Cách làm này giúp OpenAPI spec và contract test "soi gương" lẫn nhau — nếu về sau spec và contract lệch nhau, đó chính là dấu hiệu cần chạy lại bước phát hiện drift ở phần trước.
Mẹo: Đưa việc publish contract và chạy can-i-deploy vào CI pipeline như một gate bắt buộc trước khi provider được deploy lên production, không phải bước "chạy cho có". Giá trị thật của contract testing chỉ hiện ra khi nó có quyền chặn deploy — nếu chỉ chạy để tham khảo, breaking change vẫn lọt qua như bình thường.
Làm thế nào để dùng AI sinh service dependency mocking và simulation scenarios?
Backend hiện đại hiếm khi độc lập — nó gọi payment gateway, gọi service nội bộ khác, gọi third-party API (email, SMS, bản đồ, AI provider...). Test mà phụ thuộc vào các service thật này thì chậm, không ổn định (flaky vì mạng, vì rate limit), và không kiểm soát được case lỗi hiếm gặp. Mocking và simulation giải quyết vấn đề đó — và đây là mảng AI hỗ trợ rất hiệu quả vì việc sinh stub/mock config phần lớn là công việc máy móc, có pattern rõ ràng từ spec.
Dải chiến lược mocking (spectrum)
Không có một cách mock "đúng duy nhất" — lựa chọn phụ thuộc vào bạn đang test ở tầng nào:
- In-process mock/stub (ví dụ Jest
jest.mock(), Sinon, unittest.mock): nhanh nhất, dùng cho unit test, nhưng không kiểm tra được việc bạn gọi HTTP client đúng cách (headers, serialization...). - HTTP-level mock server (WireMock, MSW, Mockoon, Prism): chặn request HTTP thật, trả response giả theo rule đã định nghĩa. Phù hợp cho integration test — kiểm tra được toàn bộ tầng HTTP client, retry logic, timeout handling.
- Sandbox/test mode của third-party thật (Stripe test mode, Twilio test credentials): độ tin cậy cao nhất vì gần với hành vi thật, nhưng chậm hơn, phụ thuộc mạng, và không phải third-party nào cũng cung cấp.
- Service virtualization đầy đủ (dựng lại toàn bộ behavior của service phụ thuộc, có state, có lỗi ngẫu nhiên): dùng cho test ở tầng staging/pre-production, mô phỏng gần sát production nhất.
Nguyên tắc chọn: dùng mock ở tầng thấp nhất (rẻ nhất) vẫn đủ để phát hiện loại lỗi bạn quan tâm. Unit test không cần WireMock; nhưng test "hệ thống của tôi có retry đúng 3 lần khi payment gateway trả 503 không" thì buộc phải mock ở tầng HTTP, vì logic retry nằm trong HTTP client, không nằm trong business logic bạn có thể mock tắt.
Mẹo: Trước khi prompt AI sinh mock, hãy tự trả lời câu "tôi đang test hành vi của service TÔI, hay hành vi của service PHỤ THUỘC?". Nếu là hành vi của mình (retry, timeout, circuit breaker), phải mock ở tầng HTTP thật. Nếu chỉ cần input giả để chạy qua business logic, in-process mock là đủ và nhanh hơn nhiều.
Sinh WireMock stub từ API spec
WireMock là công cụ HTTP mock server rất phổ biến ở hệ Java/Spring nhưng dùng được với mọi ngôn ngữ (chạy như server độc lập). AI sinh stub mapping JSON của WireMock rất tốt khi có OpenAPI spec làm nguồn.
Đây là OpenAPI spec của "inventory-service" (đính kèm), tôi cần mock service
này bằng WireMock để test "order-service" (service tôi đang phát triển) mà
không cần dựng inventory-service thật.
Hãy sinh cho tôi các file WireMock stub mapping (định dạng JSON, đặt trong
thư mục mappings/) cho endpoint GET /inventory/{sku}:
1. Stub mặc định: trả 200 với response mẫu đúng theo schema trong spec, cho
sku = "SKU-1001" (còn hàng, quantity = 50).
2. Stub cho sku = "SKU-OUT-OF-STOCK": trả 200 nhưng quantity = 0.
3. Stub cho sku = "SKU-404": trả 404 theo đúng error schema trong spec.
4. Stub mô phỏng chậm: sku = "SKU-SLOW" trả 200 sau delay 8000ms (dùng
fixedDelayMilliseconds), để test timeout handling của order-service
(giả sử order-service có timeout cấu hình 5000ms).
5. Stub mô phỏng lỗi 503 liên tục cho sku = "SKU-DOWN", để test circuit
breaker của order-service có mở đúng sau N lần lỗi liên tiếp không.
Với mỗi stub, dùng request matcher chính xác theo urlPathTemplate + path
parameter sku, không dùng urlPattern regex lỏng lẻo gây match nhầm giữa
các stub.
Chi tiết quan trọng: chỉ định rõ AI dùng urlPathTemplate với path parameter thay vì regex tùy tiện — stub mapping mơ hồ là nguyên nhân phổ biến khiến nhiều test "vô tình" match nhầm stub khác, dẫn tới false positive khó debug.
Sinh Mock Service Worker (MSW) handler
MSW là công cụ mock phổ biến ở hệ JavaScript/TypeScript hiện đại (dùng được cả browser và Node), đặc biệt mạnh cho việc test tầng gọi API ở frontend hoặc BFF (backend-for-frontend), nhờ chặn request ở tầng network thay vì mock module.
Tôi dùng MSW v2 để mock API "notification-service" trong test Node.js
(Vitest) cho service "checkout-service" của tôi. Endpoint cần mock:
POST https://notification-service.internal/v1/send.
Hãy viết handler MSW cho các case:
1. Case mặc định: request có type = "order_confirmation" trả 202 Accepted
với body { messageId: <uuid>, status: "queued" }.
2. Case request thiếu field "recipientEmail" bắt buộc: trả 400 với error
body theo format { error: { code: "VALIDATION_ERROR", field: "recipientEmail" } }.
3. Case mô phỏng rate limit: sau 3 request liên tiếp trong cùng 1 test
(dùng state đếm số lần gọi trong handler), request thứ 4 trả 429 kèm
header Retry-After: 2.
4. Setup server.listen() với onUnhandledRequest: 'error' để bất kỳ request
nào không match handler nào đều làm test fail rõ ràng, thay vì đi qua
network thật một cách âm thầm.
Viết kèm ví dụ 1 test case dùng server.use() để override handler tạm thời
cho riêng 1 test (case rate limit), không ảnh hưởng tới handler mặc định
của các test khác.
Chi tiết onUnhandledRequest: 'error' rất quan trọng và hay bị bỏ quên — nếu không set, MSW mặc định cho request không match đi ra network thật (hoặc chỉ warning), khiến test "tưởng đang mock" nhưng thực ra đang gọi ra ngoài, gây flaky và đôi khi gọi nhầm vào production endpoint thật nếu base URL cấu hình sai.
Sinh chaos và failure simulation scenario
Đây là mảng nhiều team QA bỏ qua vì tốn công thiết kế: hệ thống của bạn phản ứng thế nào khi service phụ thuộc không hoạt động đúng cách — không chỉ trả lỗi rõ ràng (4xx/5xx), mà còn timeout, trả response méo (malformed), hoặc chậm bất thường. AI rất hữu ích ở việc liệt kê đầy đủ các kịch bản chaos mà con người dễ quên, và sinh code mock cho từng kịch bản.
Service "checkout-service" của tôi gọi "payment-gateway-service" qua HTTP.
Hãy liệt kê cho tôi một bộ kịch bản chaos/failure toàn diện cần test cho
loại tích hợp thanh toán này (không chỉ lỗi HTTP status thông thường), sau
đó với mỗi kịch bản, viết WireMock fault mapping hoặc MSW handler tương ứng:
1. Timeout hoàn toàn (không trả response, connection treo tới khi client
tự timeout) — dùng WireMock "fault": "CONNECTION_RESET_BY_PEER" hoặc
tương đương.
2. Response 200 nhưng body JSON bị cắt cụt/malformed (test JSON parser có
xử lý lỗi gracefully không, hay throw uncaught exception làm sập cả
request).
3. Response 200 hợp lệ về mặt HTTP nhưng chứa dữ liệu nghiệp vụ mâu thuẫn
(ví dụ status = "success" nhưng transactionId = null) — test xem
checkout-service có validate kỹ response thành công, hay tin tưởng mù
quáng vào status 200.
4. Latency tăng dần (100ms → 500ms → 3000ms → 10000ms) qua các lần gọi liên
tiếp, để test threshold nào làm timeout/circuit breaker của
checkout-service kích hoạt.
5. Service trả đúng response nhưng đóng connection ngay sau khi gửi status
line, trước khi gửi hết body (test xử lý response bị cắt giữa dòng).
6. Idempotency: gọi lại đúng request (cùng idempotency key) 2 lần liên tiếp,
payment-gateway trả 2 response khác nhau — test checkout-service có phát
hiện và xử lý mâu thuẫn này không, hay chấp nhận response thứ 2 một cách
im lặng.
Với mỗi kịch bản, giải thích rõ nó mô phỏng loại lỗi thật nào từng gặp trong
thực tế vận hành service thanh toán (network partition, downstream overload,
bug ở phía provider...).
Yêu cầu AI "giải thích nó mô phỏng loại lỗi thật nào" rất đáng giá — nó buộc bạn (và AI) neo mỗi kịch bản chaos vào một sự cố thực tế có thể xảy ra, thay vì tạo ra danh sách kịch bản kỳ lạ không phản ánh rủi ro thật của hệ thống.
Sinh stateful mock scenario
Mock đơn giản trả response cố định là chưa đủ cho nhiều luồng test — ví dụ luồng "tạo đơn hàng, sau đó gọi API check trạng thái, trạng thái phải chuyển từ pending sang confirmed sau khi webhook được gọi". Đây gọi là stateful mock — mock server nhớ trạng thái qua nhiều request, mô phỏng đúng hành vi có state của service thật.
Tôi cần mock "payment-gateway-service" theo kiểu stateful bằng WireMock
(dùng Scenario API của WireMock), mô phỏng luồng thanh toán thật:
1. Request đầu POST /payments trả 200, status = "processing", chuyển
scenario state từ "Started" sang "Payment Created".
2. Trong khi state = "Payment Created", nếu gọi GET /payments/{id}, trả
status = "processing".
3. Sau khi (giả lập) webhook xử lý xong — tôi sẽ tự gọi một request
PUT /payments/{id}/simulate-webhook để chuyển state sang "Payment Confirmed".
4. Từ lúc đó, GET /payments/{id} phải trả status = "confirmed" cho mọi
request tiếp theo trong cùng scenario.
Viết đầy đủ file WireMock stub mapping cho scenario này, dùng
"scenarioName", "requiredScenarioState", "newScenarioState" đúng cú pháp
WireMock, và viết kèm 1 test Node.js minh họa gọi đúng thứ tự 4 bước trên,
assert từng bước trả state đúng như mô tả.
Với MSW, cách làm tương đương là dùng closure/module-level variable để giữ state giữa các request trong cùng một test — AI sinh khá chuẩn nếu bạn mô tả rõ luồng chuyển trạng thái (state machine) bạn muốn mô phỏng, giống ví dụ WireMock trên.
Mẹo: Đừng lạm dụng stateful mock cho mọi trường hợp — nó phức tạp hơn để maintain và dễ gây confusion khi test fail (vì lỗi có thể do state bị "lệch" giữa các bước, không phải do code thật sai). Chỉ dùng stateful mock khi bạn thực sự cần test một luồng nhiều bước có phụ thuộc trạng thái; với phần lớn test case đơn lẻ, mock stateless (mỗi request độc lập) vẫn là lựa chọn đơn giản và ít lỗi hơn.
Tổng kết
Bốn mảng trong bài này — quản lý state database, schema/data integrity, contract testing, và mocking/simulation — đều có một điểm chung: chúng là công việc có pattern rõ ràng, lặp lại nhiều, và cần bám sát một nguồn sự thật (schema, OpenAPI spec, contract đã publish). Đó chính xác là điều kiện lý tưởng để AI phát huy tối đa giá trị — không phải để AI "quyết định" test case nào quan trọng, mà để nó rút ngắn khoảng cách từ ý tưởng ("tôi cần fixture khớp schema mới", "tôi cần mock service này trả lỗi timeout") tới code chạy được, để bạn — người QA Engineer — dồn thời gian còn lại vào việc mà AI không làm thay được: hiểu đúng business rule, và quyết định rủi ro nào thật sự đáng để test.