Đây là bài thực hành tổng hợp (capstone) của module. Tới đây bạn đã học riêng lẻ: cách sinh E2E script, cách sinh API/contract test, cách cấu trúc context codebase để AI xuất ra code chạy được ngay, cách sinh test data theo boundary value analysis, và cách bảo trì test suite sau khi sinh. Bài này ghép tất cả lại thành một buổi workshop có thật — từ một feature spec và tài liệu API còn "sống" (chưa có test nào), đi hết một vòng: phân tích spec, sinh API test, sinh E2E test, đóng gói context project, debug lỗi AI sinh ra, và commit/document đúng chuẩn team.
Tình huống giả định xuyên suốt bài: bạn được giao viết test cho feature "Áp dụng mã giảm giá (discount code) khi checkout" — một feature đã có API xong, UI xong, nhưng chưa có test nào. Ta sẽ đi từ số 0 tới một PR test suite hoàn chỉnh, sẵn sàng review.
Làm Thế Nào Để Sinh Test E2E và API Từ Feature Spec và Tài Liệu API?
Điểm khởi đầu của mọi buổi sinh test tốt không phải là mở editor viết code ngay, mà là đưa AI đọc kỹ tài liệu đầu vào trước — feature spec và API doc — để nó xây được một "mental model" đúng về nghiệp vụ trước khi viết bất kỳ dòng code nào.
Mẹo: Luôn bắt AI tóm tắt lại spec bằng lời của nó trước khi yêu cầu sinh test, và bạn đọc bản tóm tắt đó trước. Nếu bản tóm tắt sai lệch ý nghiệp vụ, sửa ngay ở bước này — sửa hiểu sai trước khi sinh code luôn rẻ hơn rất nhiều so với sửa hàng chục test case sai sau khi đã viết xong.
Bước 1: Phân Tích Feature Spec
Feature spec giả định (rút gọn) cho tính năng discount code:
## Feature: Apply Discount Code at Checkout
- User có thể nhập discount code ở trang checkout, trước khi bấm "Place Order"
- Code hợp lệ: giảm % hoặc giảm số tiền cố định, áp dụng lên subtotal
(không áp dụng lên shipping fee)
- Mỗi user chỉ áp dụng được 1 code / 1 order
- Code có ngày hết hạn (expiresAt) và giới hạn số lần dùng tổng
(usageLimit, usageCount)
- Nếu code hết hạn hoặc hết lượt dùng: hiển thị lỗi "Mã giảm giá không còn
hiệu lực", không áp dụng giảm giá
- Nếu subtotal sau giảm < 0: giảm giá bị cap lại đúng bằng subtotal
(order tối thiểu 0đ, không âm)
- Discount code có field minOrderValue: nếu subtotal < minOrderValue thì
code không áp dụng được, hiển thị lỗi kèm số tiền còn thiếu
Prompt phân tích spec trước khi sinh bất kỳ test nào:
Đây là feature spec cho tính năng "Apply Discount Code at Checkout"
(paste toàn bộ spec ở trên).
Trước khi sinh test, hãy:
1. Tóm tắt lại nghiệp vụ này bằng lời của bạn, ngắn gọn, để tôi xác nhận
bạn hiểu đúng
2. Liệt kê toàn bộ business rule dưới dạng decision table (điều kiện ->
kết quả), bao gồm cả các rule ẩn mà spec không nói rõ nhưng có thể suy
ra được (ví dụ: điều gì xảy ra nếu discount code đã bị dùng bởi chính
user này ở order trước — spec không nói rõ, hãy hỏi tôi thay vì tự đoán)
3. Liệt kê các câu hỏi cần làm rõ với Product/BA trước khi viết test, nếu có
KHÔNG sinh test case ở bước này. Chỉ phân tích.
Bước "liệt kê câu hỏi cần làm rõ" thường lộ ra các lỗ hổng nghiệp vụ thật — ví dụ AI có thể hỏi "discount code có phân biệt hoa/thường không?" hoặc "nếu 2 tab cùng áp dụng đồng thời 1 code gần hết usageLimit thì race condition xử lý sao?" — những câu hỏi này chính là edge case giá trị cho cả test và cho review logic backend.
Bước 2: Sinh API Test Từ Tài Liệu API
Với endpoint POST /api/checkout/apply-discount, giả định tài liệu OpenAPI rút gọn:
/api/checkout/apply-discount:
post:
requestBody:
content:
application/json:
schema:
type: object
required: [orderId, discountCode]
properties:
orderId: { type: string }
discountCode: { type: string }
responses:
'200':
content:
application/json:
schema:
type: object
properties:
discountAmount: { type: number }
newTotal: { type: number }
'400':
description: Invalid or expired discount code
'404':
description: Order not found
Prompt sinh API test dựa trên cả spec nghiệp vụ (bước 1) và OpenAPI (đúng kỹ thuật đã học ở bài Contract Testing của module này):
Dựa trên business rule đã phân tích ở bước trước VÀ OpenAPI schema sau
(paste schema trên), viết bộ test pytest cho endpoint
POST /api/checkout/apply-discount.
Project convention: dùng requests, base_url từ fixture `api_client`
(đã có sẵn, trả về requests.Session() có Authorization header sẵn), file
đặt tại tests/api/test_checkout_discount.py.
Bao gồm các nhóm test:
1. Happy path: code hợp lệ, giảm % và giảm số tiền cố định (2 test riêng)
2. Business rule: code hết hạn, code hết usageLimit, subtotal < minOrderValue
3. Boundary: discount làm subtotal về đúng 0, discount lớn hơn subtotal
(kỳ vọng cap về 0, không âm)
4. Contract: response 200 đúng schema (discountAmount, newTotal đều là
number, newTotal = subtotal - discountAmount + shippingFee tính đúng)
5. Error case: orderId không tồn tại (404), discountCode rỗng/thiếu (400
validation), orderId thuộc user khác (403 - kiểm tra authorization)
Mỗi test có docstring 1 dòng mô tả rõ business rule đang kiểm tra.
Bước 3: Sinh Test E2E Từ Luồng UI
Sau khi API test đã bao phủ logic nghiệp vụ ở tầng backend, E2E test không cần lặp lại toàn bộ business rule — chỉ cần đảm bảo luồng người dùng thật hoạt động đúng và UI phản hồi đúng trạng thái, tránh trùng lặp coverage gây tốn thời gian chạy CI vô ích.
Tôi đã có API test bao phủ đầy đủ business rule cho discount code (paste
lại danh sách tên test đã sinh ở bước 2, không cần paste code, chỉ tên).
Giờ viết E2E test bằng Playwright cho đúng 4 luồng người dùng sau, KHÔNG lặp
lại việc test toàn bộ business rule combination ở tầng UI (đã có ở API test):
1. User nhập code hợp lệ -> thấy discount hiển thị đúng trên UI, tổng tiền
cập nhật realtime không cần reload
2. User nhập code không hợp lệ -> thấy message lỗi đúng, không cho submit
order với code lỗi
3. User đã áp code, sau đó xoá code -> tổng tiền trở lại giá trị gốc
4. User áp code, refresh trang -> kiểm tra hành vi persist (theo spec: code
KHÔNG được giữ lại sau refresh, phải nhập lại) — đây là edge case UI-only,
API không cover được
Test file: tests/e2e/checkout/discount-code.spec.ts. Dùng Page Object
CheckoutPage đã có sẵn (tôi sẽ cung cấp interface ở bước sau).
Mẹo: Luôn nói rõ với AI "API test đã cover gì" trước khi sinh E2E test. Nếu không, AI mặc định sinh E2E test lặp lại toàn bộ ma trận business rule ở tầng UI — làm CI chạy chậm hơn nhiều lần mà không tăng thêm độ tin cậy, vì logic đó vốn nằm ở backend, UI chỉ cần hiển thị đúng.
Làm Thế Nào Để Cung Cấp Context Codebase Sao Cho Output Chạy Được Ngay?
Đây là bước hay bị bỏ qua nhất trong thực tế — và cũng là nguyên nhân số một khiến code AI sinh ra "gần đúng nhưng không chạy được ngay". Không đưa đủ context, AI sẽ tự đoán tên fixture, tự đoán cấu trúc import, tự bịa ra Page Object không tồn tại.
Mẹo: Lưu Project Test Context Package thành file thật trong repo (ví dụ tests/CONTEXT.md), đừng chỉ giữ trong lịch sử chat — và bắt buộc review/update file này mỗi khi PR đổi convention, thêm fixture mới, hoặc đổi selector strategy. Một context package lỗi thời còn hại hơn không có context, vì AI sẽ tự tin dùng thông tin sai (ví dụ tên fixture đã đổi) mà không hề nghi ngờ, khiến code sinh ra "trông đúng" nhưng fail ngay khi chạy.
Gói Context Đầy Đủ (The Complete Context Package)
Cách hiệu quả nhất là chuẩn bị trước một tài liệu "Project Test Context Package" — viết một lần, dùng lại cho mọi lần prompt AI sinh test trong suốt buổi workshop (và về sau cho cả team).
## Project Identity
- Frontend: React 18 + TypeScript, backend: Node.js + Express + PostgreSQL
- Test framework: Playwright (E2E), pytest (API)
- Repo: monorepo, E2E test tại tests/e2e/, API test tại tests/api/
## Directory Structure
tests/
e2e/
pages/ # Page Object classes
fixtures.ts # custom Playwright fixtures
checkout/
api/
conftest.py # pytest fixtures
factories.py # data factories
## Critical Import Rule
- E2E test PHẢI import `test`, `expect` từ '../fixtures', KHÔNG từ
'@playwright/test' trực tiếp (fixtures.ts re-export có mở rộng thêm
fixture riêng của project)
- API test dùng conftest.py tự động, KHÔNG cần import fixture thủ công
## Playwright Config Summary
- baseURL: đọc từ process.env.E2E_BASE_URL
- Mỗi test tự chạy trong 1 browser context riêng (không share storage state
trừ khi dùng fixture authenticatedPage)
- Timeout mặc định: 30s/test, action timeout 10s
## Fixture Interfaces
- `authenticatedPage` (Playwright): trả về Page đã login sẵn user thường
- `seededOrder` (Playwright): tạo order có sẵn trong DB, trả về { id, subtotal }
- `api_client` (pytest): requests.Session() có auth header sẵn
- `test_order` (pytest): tạo order qua API, tự cleanup sau test
## Page Object Interfaces
- CheckoutPage (tests/e2e/pages/checkout.page.ts):
constructor(page: Page)
fillDiscountCode(code: string): Promise<void>
submitOrder(): Promise<void>
getDisplayedTotal(): Promise<string>
getErrorMessage(): Promise<string | null>
## Factory Interfaces
- buildDiscountCode(overrides?): DiscountCode (TypeScript, tests/e2e/factories.ts)
- build_order_payload(**overrides) (Python, tests/api/factories.py)
## Environment Variables
E2E_BASE_URL, API_BASE_URL, TEST_ACCOUNT_EMAIL, TEST_ACCOUNT_PASSWORD
## Selector Strategy (Priority Order)
1. getByTestId (ưu tiên cao nhất, toàn bộ element tương tác đều có data-testid)
2. getByRole kèm accessible name (cho input/button không có data-testid do
dùng component thư viện UI bên thứ ba)
3. KHÔNG dùng CSS class selector, KHÔNG dùng XPath
## Test File Naming
- E2E: kebab-case, hậu tố .spec.ts, đặt theo feature (discount-code.spec.ts)
- API: snake_case, tiền tố test_, đặt theo resource (test_checkout_discount.py)
## Known DOM Structure (from current application)
- Input mã giảm giá: data-testid="discount-code-input"
- Nút áp dụng: data-testid="apply-discount-button"
- Vùng hiển thị lỗi: data-testid="discount-error-message"
- Vùng hiển thị tổng tiền: data-testid="checkout-total-amount"
Có gói context này, mọi prompt sinh test trong workshop chỉ cần mở đầu bằng: "Dựa trên Project Test Context Package đã cung cấp, sinh test cho..." — không cần lặp lại chi tiết mỗi lần.
Cung Cấp DOM Snapshot Cho Feature Mới
Với feature mới hoàn toàn chưa có trong "Known DOM Structure" ở trên, cần trích DOM thật ngay lúc đó, không đoán, không chờ tài liệu cập nhật (tài liệu luôn trễ hơn code thật).
// scripts/extract-testids.ts — chạy 1 lần để lấy DOM thật của trang checkout
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(process.env.E2E_BASE_URL + '/checkout');
await page.waitForLoadState('networkidle');
const testIds = await page.evaluate(() => {
const elements = document.querySelectorAll('[data-testid]');
return Array.from(elements).map((el) => ({
testId: el.getAttribute('data-testid'),
tag: el.tagName.toLowerCase(),
role: el.getAttribute('role'),
text: el.textContent?.trim().slice(0, 50),
}));
});
console.log(JSON.stringify(testIds, null, 2));
await browser.close();
})();
Chạy npx tsx scripts/extract-testids.ts, dán kết quả JSON thẳng vào prompt sinh test kế tiếp — AI sẽ dùng đúng data-testid thật đang tồn tại, không đoán.
Checklist "Chạy Được Ngay" (Immediate Runability Checklist)
Trước khi chạy thử code AI vừa sinh, lướt nhanh checklist này — quét bằng mắt 30 giây thường bắt được lỗi trước khi tốn thời gian chạy:
- Import đúng từ
../fixtures, không import trực tiếp@playwright/test? - Tên fixture dùng trong test khớp đúng tên đã khai báo trong gói context (không bị AI tự đổi tên, ví dụ
seededOrderthànhseedOrder)? - Không có
data-testidnào bị bịa ra ngoài danh sách đã cung cấp? - Không có biến môi trường nào được dùng mà không có trong danh sách đã khai báo?
- Tên file test đúng convention naming đã quy định?
Làm Thế Nào Để Debug và Sửa Lỗi Khi Test Do AI Sinh Bị Fail?
Test AI sinh ra hiếm khi chạy pass 100% ngay lần đầu — điều này bình thường, không phải dấu hiệu AI tệ. Điểm khác biệt giữa engineer giỏi và không giỏi trong workflow này là tốc độ triage: biết ngay lỗi thuộc nhóm nào để không tốn thời gian sửa sai hướng.
Khung Phân Loại Lỗi (Failure Triage Framework)
Khi test mới sinh fail, luôn phân loại theo 4 nhóm trước khi sửa bất cứ gì:
- Lỗi cú pháp/import — dễ nhất, thường do thiếu context (đã giảm mạnh nếu làm đúng phần trên).
- Lỗi hiểu sai selector/DOM thật — AI đoán sai
data-testidhoặc cấu trúc DOM thực tế khác spec. - Lỗi hiểu sai business rule — test hiểu sai spec, assertion sai kỳ vọng (khác với app bug thật).
- Bug thật trong application — test đúng, phát hiện ra lỗi thật trong code cần sửa product, không sửa test.
Mẹo: Luôn tự tay xác nhận nhóm lỗi bằng cách đọc trực tiếp response/DOM thật (chạy --debug của Playwright hoặc in response JSON thật của API), đừng hỏi AI "lỗi này là gì" và tin ngay câu trả lời — AI không thấy được state thực tế đang chạy, nó chỉ đọc log bạn paste vào, nên rất dễ đoán sai giữa nhóm 3 và nhóm 4 nếu log không đủ chi tiết.
Prompt Sửa Lặp (The Iterative Fix Prompt)
Test sau đang fail (paste toàn bộ test code + error message + stack trace
đầy đủ, không rút gọn):
<paste test + error>
Đây là response thật của API khi tôi gọi trực tiếp bằng curl để đối chiếu
(paste response JSON thật):
<paste response thật>
So sánh assertion trong test với response thật ở trên. Xác định: đây là
lỗi test hiểu sai response shape, hay response thật đang thiếu field mà
spec yêu cầu phải có (khả năng là bug thật). Nêu rõ kết luận trước, sau đó
mới đề xuất sửa.
Yêu cầu "nêu rõ kết luận trước" quan trọng vì nó buộc AI phải cam kết một chẩn đoán rõ ràng (test sai hay app sai) trước khi nhảy vào sửa code — tránh tình trạng AI sửa test để nó pass mà không thực sự biết response thật có đúng nghiệp vụ không, dẫn tới việc vô tình che giấu bug thật.
Debug Vấn Đề Timing và Flakiness
Nhóm lỗi khó nhất là lỗi không nhất quán — pass khi chạy riêng, fail khi chạy cùng suite, hoặc fail ngẫu nhiên trên CI nhưng luôn pass ở máy local.
Test này fail ngẫu nhiên (khoảng 1/5 lần chạy) trên CI, luôn pass ở máy
local (paste test code).
Đây không phải lỗi logic — nghi ngờ là race condition hoặc thiếu wait điều
kiện đúng cách. Rà lại toàn bộ test, chỉ ra:
1. Chỗ nào đang dùng waitForTimeout cứng (anti-pattern) thay vì chờ điều
kiện cụ thể
2. Chỗ nào đang assert ngay sau một action async mà không chờ UI phản hồi
xong (ví dụ click submit rồi assert ngay redirect mà không chờ network
response hoàn tất)
3. Chỗ nào có thể bị ảnh hưởng bởi animation/transition CSS chưa hoàn tất
khi Playwright đã coi element "visible"
Sửa lại dùng expect().toBeVisible()/toHaveText() với auto-retry built-in
của Playwright thay cho assertion tĩnh, và page.waitForResponse() thay cho
waitForTimeout ở chỗ đang chờ network.
Debug Vấn Đề Dữ Liệu/State
Một nguồn lỗi phổ biến khác: test fail vì dữ liệu để lại từ lần chạy trước (không cleanup đúng) hoặc vì hai test chạy song song cùng đụng vào một record.
Test test_apply_discount_exceeds_usage_limit fail không nhất quán khi
chạy full suite với pytest-xdist (parallel), nhưng pass khi chạy riêng lẻ
một mình (paste test code + fixture liên quan).
Nghi ngờ: nhiều test parallel đang dùng chung 1 discount code cố định trong
DB (hardcode "SUMMER10"), gây tranh chấp usageCount giữa các worker.
Sửa để mỗi lần gọi factory tạo discount code MỚI với code string unique
(dùng uuid), không dùng chung code cố định giữa các test, đảm bảo test này
chạy an toàn dưới pytest-xdist với bất kỳ số worker nào.
Làm Thế Nào Để Commit và Document Test Suite Do AI Sinh?
Bước cuối của workshop — đưa test suite từ máy cá nhân thành một PR sẵn sàng review, đúng chuẩn của team, để reviewer (con người) tin tưởng và duyệt nhanh.
Chiến Lược Commit
Không commit toàn bộ test suite trong 1 commit khổng lồ — chia theo lớp để reviewer dễ theo dõi logic, và để nếu cần revert một phần thì không ảnh hưởng phần còn lại.
Ví dụ cấu trúc commit cho việc thêm trọn bộ test suite discount code:
commit 1: "test(api): add discount code business rule tests"
- tests/api/test_checkout_discount.py
- tests/api/factories.py (thêm build_discount_code_payload)
commit 2: "test(e2e): add discount code UI flow tests"
- tests/e2e/checkout/discount-code.spec.ts
- tests/e2e/pages/checkout.page.ts (mở rộng thêm method mới)
commit 3: "test(fixtures): add seededDiscountCode fixture and factory helpers"
- tests/e2e/fixtures.ts (thêm fixture seededDiscountCode)
- tests/e2e/factories.ts (thêm buildDiscountCode)
Mẹo: Luôn commit factory/fixture TRƯỚC hoặc CÙNG với test đầu tiên dùng nó, không commit sau — nếu commit fixture riêng ở một PR khác merge sau, reviewer đọc test ở PR trước sẽ không compile được/không hiểu context, gây review sai lệch.
Mẫu PR Description Cho Test Do AI Sinh
Minh bạch về việc dùng AI trong PR không phải điều cần né tránh — ngược lại, nó giúp reviewer biết cần soát kỹ phần nào hơn (thường là business logic assertion, ít lo phần cú pháp).
## Summary
- Thêm bộ test API (pytest) và E2E (Playwright) cho feature "Apply Discount
Code at Checkout"
- Test được sinh có hỗ trợ AI dựa trên feature spec (link) và OpenAPI schema
hiện tại (link), đã review và chỉnh sửa thủ công phần business rule
boundary (cap giảm giá về 0) và fix 1 vấn đề flaky do dùng chung discount
code giữa test song song
- Không tìm thấy bug thật trong quá trình viết test; đã làm rõ 2 điểm
nghiệp vụ chưa rõ trong spec với PM (đính kèm link thread)
## Test Plan
- [x] Chạy `pytest tests/api/test_checkout_discount.py -v` — 14/14 pass
- [x] Chạy `npx playwright test tests/e2e/checkout/discount-code.spec.ts` —
4/4 pass, chạy lại 10 lần liên tiếp không flaky
- [x] Chạy full suite hiện có, xác nhận không phá test nào khác
(`npx playwright test` toàn bộ: 142/142 pass)
- [x] Đã review thủ công toàn bộ assertion liên quan tới business rule cap
giảm giá về 0 và minOrderValue, đối chiếu đúng spec
- [ ] Cần QA lead review riêng phần boundary "subtotal đúng bằng minOrderValue"
(đường biên nhạy cảm, muốn có second opinion trước khi merge)
Documentation Trong Code (Inline)
Với test do AI sinh, comment giải thích "tại sao" quan trọng hơn "làm gì" — vì code test tự nó đã mô tả hành động, nhưng lý do chọn đúng giá trị boundary hay đúng cách mock thường không tự hiển nhiên với người đọc sau này.
test('should cap discount to exactly the subtotal when discount exceeds order value', async ({ page, seededOrder }) => {
// Business rule (spec section "Boundary handling"): nếu discountAmount tính
// ra lớn hơn subtotal, hệ thống phải cap lại đúng bằng subtotal, không cho
// total âm. Test này dùng order có subtotal thấp (50,000đ) và code giảm cố
// định cao hơn (100,000đ) để buộc rơi vào nhánh cap này.
const checkoutPage = new CheckoutPage(page);
await checkoutPage.fillDiscountCode('FIXED100K');
await checkoutPage.submitOrder();
const total = await checkoutPage.getDisplayedTotal();
expect(total).toBe('0đ');
});
Sinh Documentation Cho Test Bằng AI
Sau khi toàn bộ suite đã pass và review xong, có thể giao AI viết tài liệu tổng quan test suite — hữu ích cho onboarding người mới hoặc cho chính bạn 6 tháng sau khi đã quên chi tiết.
Đọc toàn bộ 3 file test đã hoàn thiện: tests/api/test_checkout_discount.py,
tests/e2e/checkout/discount-code.spec.ts, và các fixture/factory liên quan
(paste nội dung cả 3 file).
Viết một file README.md ngắn đặt tại tests/checkout/README.md, gồm:
- Tổng quan feature được test (2-3 câu)
- Bảng liệt kê từng test case, thuộc layer nào (API/E2E), business rule nào
đang cover
- Ghi rõ những gì E2E KHÔNG cover vì đã cover ở API (để người sau không hiểu
lầm và viết trùng)
- Hướng dẫn chạy riêng suite này (command cụ thể) và cách chạy kèm biến môi
trường cần thiết
Tài liệu này khép lại vòng đời của một test suite hoàn chỉnh: từ đọc spec, sinh test có context đúng, debug lỗi có phân loại rõ ràng, tới commit và document — đúng như quy trình một QA engineer senior thực hiện trong công việc thật, chỉ khác là mỗi bước đều được AI hỗ trợ tăng tốc, còn tư duy kiểm soát chất lượng vẫn hoàn toàn nằm ở con người.