Điểm khác biệt lớn nhất giữa một QA Engineer mới dùng AI và một QA Engineer đã thành thạo không nằm ở việc "biết prompt", mà nằm ở khả năng cấu trúc context (ngữ cảnh) codebase sao cho AI sinh ra test chạy được ngay (runnable) và khớp với convention thực tế của dự án. Không có context đúng, AI sẽ sinh test "đẹp" về mặt code nhưng sai hoàn toàn về thực tế — gọi hàm không tồn tại, dùng POM (Page Object Model) sai interface, hoặc phá vỡ convention naming của team. Chủ đề này đi sâu vào cách xây dựng context hierarchy, dùng file CLAUDE.md (hoặc tương đương) để duy trì context lâu dài, và cách phát hiện/ngăn chặn test "generic" không ăn khớp với codebase.
Cần cung cấp Context Codebase nào để AI sinh Test hiểu đúng dự án?
Không phải mọi thông tin trong codebase đều cần đưa vào context cho AI — đưa quá ít thì AI đoán mò, đưa quá nhiều thì lãng phí context window (vùng ngữ cảnh LLM xử lý được trong một lần) và làm loãng những phần quan trọng. Kỹ năng cốt lõi là biết chọn đúng lớp context cần thiết cho từng loại task.
Thứ Bậc Context (Context Hierarchy)
Context cho AI nên được tổ chức theo các lớp từ tổng quát đến cụ thể:
- Project-level: framework testing đang dùng, ngôn ngữ, cấu trúc folder tổng thể, cách chạy test (script trong package.json)
- Module-level: Page Object Model liên quan đến flow đang test, API client wrapper, fixture đang có
- File-level: 1-2 file test tương tự đã viết trước đó trong cùng module, làm mẫu về style
- Task-level: yêu cầu cụ thể của test case đang cần sinh (flow, data, assertion)
Việc thiếu lớp Module-level là nguyên nhân phổ biến nhất khiến AI tự bịa ra Page Object hoặc helper function không tồn tại, vì nó không có gì để tham chiếu ngoài kiến thức chung.
Xây Dựng Context Snapshot
Trước khi bắt đầu 1 phiên làm việc sinh test dài (nhiều file, nhiều giờ), nên chuẩn bị trước 1 "context snapshot" — tập hợp các đoạn code/tài liệu cố định sẽ dùng lại nhiều lần trong session, gồm:
- Nội dung file cấu hình test (
playwright.config.ts,robot.yaml...) - Interface/type định nghĩa của các POM, fixture, helper chính
- 1 file test mẫu "vàng" (golden example) thể hiện đúng convention team muốn
Đây là context cho toàn bộ session làm test hôm nay, hãy nhớ và áp dụng
xuyên suốt các yêu cầu sau của tôi:
1. Framework: Playwright + TypeScript, chạy bằng npx playwright test
2. Base URL: process.env.BASE_URL (không hard-code URL)
3. Page Object nằm ở tests/pages/*.ts, mỗi class export theo pattern:
export class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) { ... }
}
4. Fixture custom nằm ở tests/fixtures/*.fixture.ts
5. File test mẫu chuẩn convention (tests/checkout/checkout-cod.spec.ts):
[dán nội dung file mẫu]
Từ giờ, mọi test tôi yêu cầu sinh phải tuân theo đúng các quy ước trên.
Mẫu (Pattern) CLAUDE.md Để Duy Trì Context Lâu Dài
Nhiều công cụ AI coding hiện đại (Claude Code, Cursor, Copilot Workspace...) hỗ trợ đọc một file context cố định ở gốc repo hoặc gốc thư mục test (thường gọi là CLAUDE.md hoặc tương đương) — nội dung file này tự động được nạp vào context mỗi khi AI làm việc trong thư mục đó, không cần bạn dán lại mỗi lần prompt. Đây là cách hiệu quả nhất để duy trì context nhất quán qua nhiều session, nhiều người trong team.
Ví dụ một file tests/CLAUDE.md cho project E2E:
## Framework
- Playwright Test + TypeScript
- Chạy: `npx playwright test`, chạy 1 file: `npx playwright test tests/checkout/checkout-cod.spec.ts`
- CI chạy trên 3 browser: chromium, webkit, firefox (playwright.config.ts)
## Key Rules
- Luôn dùng data-testid cho selector, KHÔNG dùng CSS class
- KHÔNG dùng waitForTimeout() cố định
- Mọi test phải độc lập (không phụ thuộc thứ tự chạy)
## Existing POMs
- tests/pages/LoginPage.ts
- tests/pages/CheckoutPage.ts
- tests/pages/CartPage.ts
(xem interface đầy đủ trong từng file trước khi viết test mới dùng POM)
## Fixture Usage
- tests/fixtures/auth.fixture.ts export fixture `authPage`
- tests/fixtures/api.fixture.ts export fixture `apiContext` (đã có sẵn
Authorization header cho test user)
## Test File Naming
- Tên file: {feature}-{scenario}.spec.ts (kebab-case)
- Test case description viết tiếng Việt không dấu HOẶC tiếng Anh
(theo convention hiện tại của team, KHÔNG trộn 2 ngôn ngữ trong 1 file)
## Running Tests
- Local: `npm run test:e2e`
- CI: chạy qua GitHub Actions, xem .github/workflows/e2e.yml
Mẹo: Đừng viết file CLAUDE.md một lần rồi bỏ quên — hãy coi nó như một phần của codebase cần review trong PR. Khi convention team thay đổi (ví dụ đổi chiến lược selector, thêm fixture mới), cập nhật CLAUDE.md ngay trong cùng PR đó, nếu không AI sẽ tiếp tục sinh test theo convention cũ đã lỗi thời.
Làm sao giúp AI hiểu đúng Page Object Model và Test Helper Utilities của bạn?
Page Object Model (POM) là pattern phổ biến để đóng gói tương tác UI thành class riêng, nhưng AI chỉ dùng đúng POM khi nó thấy được interface thực tế — nếu không, nó sẽ tự "phát minh" method dựa trên tên class nghe hợp lý, dẫn đến lỗi Property 'xxx' does not exist on type.
Cung Cấp Interface POM Cho AI
Cách đáng tin cậy nhất là dán nguyên văn phần khai báo public method của class POM (không cần dán toàn bộ implementation nếu quá dài) vào prompt:
Đây là interface đầy đủ của CheckoutPage (tests/pages/CheckoutPage.ts),
chỉ dùng đúng các method này, KHÔNG tự tạo method mới:
export class CheckoutPage {
constructor(private page: Page) {}
async fillShippingAddress(data: { name: string; phone: string; address: string }): Promise<void>;
async selectPaymentMethod(method: 'COD' | 'CREDIT_CARD' | 'MOMO'): Promise<void>;
async placeOrder(): Promise<void>;
async getOrderIdFromUrl(): Promise<string>;
getErrorMessage(): Locator;
}
Viết test checkout với phương thức MOMO, verify khi API thanh toán MOMO
timeout (mock trả 504), UI hiển thị đúng error message qua getErrorMessage().
Nếu bạn không dán interface mà chỉ nói "dùng CheckoutPage có sẵn", AI nhiều khả năng sẽ tự viết checkoutPage.selectMomo() hoặc checkoutPage.pay('momo') — nghe hợp lý nhưng không khớp method thật.
Cung Cấp Interface API Client Và Database Helper
Tương tự với POM, các helper gọi API hoặc truy vấn database trong test cũng cần được expose interface rõ ràng:
Đây là API client helper (tests/helpers/api-client.ts):
export class ApiClient {
constructor(private baseURL: string, private token: string) {}
async createOrder(payload: CreateOrderPayload): Promise<OrderResponse>;
async getOrder(orderId: string): Promise<OrderResponse>;
async deleteOrder(orderId: string): Promise<void>;
}
Và database helper (tests/helpers/db-helper.ts):
export async function seedOrder(data: Partial<Order>): Promise<Order>;
export async function cleanupOrdersByUser(userId: string): Promise<void>;
Viết test cho flow: seed 1 order qua seedOrder(), gọi apiClient.getOrder()
verify data khớp, sau đó cleanup bằng cleanupOrdersByUser() trong afterEach.
Mẹo: Nếu POM hoặc helper của bạn có quá nhiều method (hơn 15-20), đừng dán toàn bộ — hãy trích riêng những method liên quan trực tiếp đến flow đang test. Dán dư thừa không sai về logic nhưng làm AI dễ "nhặt nhầm" method không liên quan, hoặc kết hợp method theo cách không hợp lý về nghiệp vụ.
Làm sao cấu hình AI tuân theo Convention Test Framework của dự án?
Mỗi team có convention riêng: cách đặt tên, cách tổ chức file, cách viết assertion, cách xử lý async... AI mặc định sẽ áp dụng convention "phổ biến nhất" nó học được từ dữ liệu training — thường không khớp 100% với convention cụ thể của dự án bạn. Cần chủ động enforce convention qua cả prompt và cấu hình hệ thống của tool AI.
Định Nghĩa Convention Constraint
Trước khi enforce, cần liệt kê rõ ràng, cụ thể (không mơ hồ) từng convention đang áp dụng: naming convention cho file/describe/test case, cách import, cách tổ chức assertion (arrange-act-assert), cách xử lý config/env, và style code (dùng async/await hay .then(), dùng arrow function hay function keyword...).
Enforce Convention Qua Prompt System
Với các tool hỗ trợ system prompt/project rules riêng (ví dụ Cursor rules, Claude Code CLAUDE.md, Copilot custom instructions), hãy đặt convention vào đó thay vì lặp lại mỗi lần chat. Ví dụ file tests/CLAUDE.md mở rộng thêm phần convention:
## Conventions That Must Be Followed
- Mỗi test file bắt đầu bằng import từ '../fixtures', không import trực
tiếp từ '@playwright/test' trừ khi cần type
- test.describe tên phải là tên feature bằng tiếng Anh, PascalCase
không dấu cách (ví dụ: CheckoutFlow, không phải "checkout flow")
- Assertion luôn dùng expect().toXxx(), không dùng assert() của Node.js
- Mọi API call trong test phải có timeout rõ ràng, không dùng default
## Never Do These (I will reject PRs that contain these patterns)
- page.waitForTimeout() với số cố định
- Selector CSS class hoặc XPath
- Hard-code baseURL hoặc token trong file test
- Test phụ thuộc thứ tự chạy (test B cần test A chạy trước để có data)
- console.log() còn sót lại trong code test đã merge
## Always Do These
- Mỗi test file có JSDoc comment ngắn ở đầu mô tả mục đích test suite
- Dùng test.step() để chia nhỏ các bước lớn trong 1 test case dài
- Cleanup data test trong afterEach, không dồn vào afterAll
## Convention Verification Prompt
Trước khi trả kết quả, tự kiểm tra lại: code vừa sinh có vi phạm mục nào
trong "Never Do These" không? Nếu có, sửa lại trước khi đưa cho tôi.
Phần "Convention Verification Prompt" ở cuối là kỹ thuật self-check — yêu cầu AI tự rà lại output của chính nó theo checklist trước khi trả kết quả, giảm đáng kể tỷ lệ vi phạm convention cần bạn sửa lại sau.
Mẹo: Viết convention dưới dạng "Never Do These" cụ thể, có ví dụ code sai kèm theo, hiệu quả hơn nhiều so với chỉ nói chung "hãy viết code sạch" hoặc "theo best practice". AI phản hồi tốt với rule cụ thể, có thể kiểm chứng (verifiable), hơn là các khái niệm trừu tượng.
Làm sao ngăn AI sinh Test Generic không khớp với Codebase?
Test "generic" là loại test AI sinh dựa trên pattern chung của cả internet, không thực sự gắn với đặc thù codebase của bạn — chạy được về cú pháp nhưng không test đúng cái cần test, hoặc test những thứ không tồn tại trong hệ thống thực tế.
Năm Mẫu Test Generic Phổ Biến Và Cách Ngăn Chặn
-
Test giả định API/UI generic: AI sinh test cho
/api/userstheo REST chuẩn "sách giáo khoa" trong khi API thực tế của bạn có cấu trúc khác (ví dụ dùng POST cho cả update). → Ngăn bằng cách luôn đưa OpenAPI spec/route definition thực tế, không để AI đoán theo convention REST chung. -
Test chỉ check happy path, bỏ qua business rule đặc thù: AI không biết rule riêng của domain (ví dụ "chỉ hủy đơn khi status = pending trong vòng 24h"). → Ngăn bằng cách mô tả rõ business rule trong prompt hoặc trỏ tới file logic liên quan.
-
Test dùng mock data không đại diện: AI tự sinh data như "John Doe", "test@test.com" không phản ánh định dạng dữ liệu thực (ví dụ số điện thoại VN, mã đơn hàng theo pattern riêng). → Ngăn bằng cách cung cấp ví dụ data thực tế hoặc factory function có sẵn.
-
Test đặt sai vị trí/tên file không theo cấu trúc project: AI đặt file test ở gốc thư mục hoặc dùng tên không theo convention. → Ngăn bằng cách nêu rõ đường dẫn file mong muốn trong mọi prompt.
-
Test trùng lặp logic đã có sẵn trong helper: AI viết lại logic login/setup thủ công bằng cách click từng bước UI, trong khi project đã có sẵn API-based login helper nhanh hơn nhiều cho việc setup precondition. → Ngăn bằng cách luôn giới thiệu helper có sẵn trước khi yêu cầu sinh test mới.
Prompt Kiểm Tra Khả Năng Chạy Được (Runability Check)
Sau khi AI sinh test, có thể yêu cầu nó tự đối chiếu lại với codebase (nếu tool có quyền đọc file) để phát hiện sớm phần không khớp:
Trước khi hoàn tất, hãy tự kiểm tra lại test vừa viết bằng cách đối chiếu:
1. Mọi method POM được gọi có thực sự tồn tại trong tests/pages/*.ts không?
2. Mọi API endpoint được gọi có khớp với route thực tế trong openapi.yaml không?
3. Import path có đúng với cấu trúc folder thực tế của project không?
4. Có field/property nào trong data test không có trong type definition không?
Nếu phát hiện bất kỳ điểm nào không khớp, liệt kê ra và đề xuất sửa
trước khi tôi chạy test này.
Dùng Test Fail Thật Làm Negative Context
Một kỹ thuật nâng cao và khá hiệu quả: đưa cho AI 1 ví dụ test đã từng fail thật trong quá khứ (cùng lý do vì sao nó sai — ví dụ sai selector, giả định sai business rule) như một "negative example" (ví dụ về điều không nên làm), giúp AI tránh lặp lại đúng lỗi đó:
Tham khảo test này đã từng gây lỗi flaky trên CI (đã fix, giữ lại làm ví dụ):
// SAI - waitForTimeout cố định, gây flaky khi CI chậm
await page.waitForTimeout(3000);
await expect(page.getByText('Đặt hàng thành công')).toBeVisible();
// ĐÚNG - chờ đúng điều kiện
await expect(page.getByTestId('order-success-banner')).toBeVisible({ timeout: 10000 });
Khi viết test mới cho flow tương tự, tuyệt đối không lặp lại pattern SAI ở trên.
Mẹo: Lưu lại một thư mục nhỏ trong repo (ví dụ docs/ai-context/anti-patterns.md) tập hợp các ví dụ lỗi thật đã từng xảy ra khi dùng AI sinh test, kèm bản fix đúng. Theo thời gian, thư mục này trở thành "bộ nhớ tập thể" của team về những gì AI trong dự án của bạn thường sai — dán vào context sẽ hiệu quả hơn nhiều so với các lời khuyên general tìm được trên mạng.