Một trong những công việc tốn thời gian nhất của QA không phải là viết test case, mà là chuẩn bị dữ liệu cho test case đó. Bạn cần tên người hợp lệ theo từng vùng, số điện thoại đúng format, ngày sinh nằm ở các mốc biên nhạy cảm, file upload đúng kích thước tối đa cho phép... Tất cả đều là những việc lặp đi lặp lại, dễ sai, và rất tốn công nếu làm tay. Đây chính là mảnh đất mà LLM (large language model) phát huy tối đa giá trị: nó có kiến thức domain rộng, hiểu format dữ liệu theo locale, và có thể sinh ra hàng trăm biến thể trong vài giây — miễn là bạn biết cách prompt đúng và biết giới hạn của nó ở đâu.
Bài này đi sâu vào bốn nhóm kỹ năng: sinh dữ liệu test thực tế theo domain, sinh dữ liệu biên/edge case theo tư duy tấn công (adversarial), xây data factory và fixture tái sử dụng, và — quan trọng không kém — biết dữ liệu nào tuyệt đối không được đưa vào AI.
Làm Thế Nào Để Sinh Bộ Test Data Thực Tế, Đúng Chuẩn Domain Với AI?
Test data "thực tế" nghĩa là dữ liệu trông giống dữ liệu sản xuất thật: đúng định dạng, đúng ràng buộc nghiệp vụ, đúng phân phối thống kê tương đối (ví dụ không phải ai cũng tên "Test User 1", "Test User 2"). AI làm việc này tốt hơn hẳn các thư viện fake data generic như Faker.js nếu bạn cung cấp đủ context domain, vì nó có thể suy luận ràng buộc chéo giữa các field (ví dụ ngày hết hạn thẻ phải sau ngày cấp thẻ, mã tỉnh phải khớp với đầu số điện thoại).
Mẹo: Đừng bao giờ prompt AI sinh dữ liệu bằng một câu chung như "generate 50 fake users" — kết quả sẽ trùng lặp pattern rất lộ (email toàn @example.com, tên toàn "John Doe biến thể"). Luôn ép AI đa dạng hoá bằng cách chỉ định rõ số lượng nhóm biến thể (ví dụ: 10 user Việt Nam, 10 user Nhật, 10 user có tên có dấu đặc biệt, 10 user email chứa "+" alias) — việc chia nhóm buộc model phải thực sự thay đổi phân phối dữ liệu chứ không copy-paste template.
Prompt Ngữ Cảnh Domain (The Domain Context Prompt)
Chìa khoá để có dữ liệu chất lượng là "domain context prompt" — một đoạn prompt mô tả đầy đủ nghiệp vụ trước khi yêu cầu sinh dữ liệu. Cấu trúc gồm: (1) mô tả entity và ý nghĩa nghiệp vụ, (2) liệt kê ràng buộc từng field, (3) liệt kê ràng buộc chéo field, (4) yêu cầu format output.
Ví dụ prompt thực tế cho một ứng dụng ngân hàng số:
Bạn là chuyên gia sinh test data cho hệ thống ngân hàng số tại Việt Nam.
Sinh 20 bản ghi "chủ thẻ tín dụng" với các ràng buộc sau:
- fullName: tên người Việt có dấu, độ dài 2-5 từ, không dùng tên đã quá phổ biến
trong bộ dữ liệu mẫu (tránh lặp "Nguyễn Văn A")
- dateOfBirth: tuổi chủ thẻ phải từ 18 đến 65 (tính theo ngày hôm nay 2026-07-09)
- nationalId: đúng format CCCD 12 số của Việt Nam, 3 số đầu là mã tỉnh hợp lệ
(dùng danh sách 063 = Bến Tre, 001 = Hà Nội, 079 = TP.HCM...)
- phoneNumber: đầu số hợp lệ của nhà mạng VN (090, 091, 098, 032...), 10 số
- creditLimit: phân phối lệch phải — 60% trong khoảng 10-30 triệu,
30% trong khoảng 30-100 triệu, 10% trên 100 triệu (khách VIP)
- cardIssueDate và cardExpiryDate: expiry luôn đúng 5 năm sau issue date,
issue date trong vòng 3 năm gần đây
Output dạng JSON array, mỗi object đúng theo schema trên. Không thêm giải thích.
Việc nêu rõ tỷ lệ phân phối (60/30/10) là điểm khác biệt lớn nhất so với dùng Faker — bạn kiểm soát được "shape" của dữ liệu để test các báo cáo, dashboard, hoặc logic phân nhóm khách hàng.
Sinh Test Data Có Cấu Trúc Cho Entity Cụ Thể
Với các entity phức tạp có quan hệ (ví dụ Order có nhiều OrderItem, Order thuộc về Customer), nên yêu cầu AI sinh dữ liệu theo cấu trúc lồng nhau nhất quán về khoá ngoại thay vì sinh từng bảng riêng rồi tự ghép — dễ sai và tốn công đối chiếu.
Sinh 5 đơn hàng (Order) hoàn chỉnh cho hệ thống e-commerce, dạng JSON, theo schema:
Order {
orderId: string (format "ORD-{8 số}")
customerId: string (format "CUST-{6 số}")
customerEmail: string (khớp domain với customerId, không trùng nhau giữa các order)
items: OrderItem[] (2-6 item mỗi order)
subtotal: number (= tổng items[].price * items[].qty, tính đúng)
shippingFee: number (30000 nếu subtotal < 500000, ngược lại 0)
total: number (= subtotal + shippingFee, tính đúng, không sai lệch)
status: enum ["pending","paid","shipped","delivered","cancelled"]
createdAt: ISO 8601 timestamp trong 30 ngày gần nhất tính từ 2026-07-09
}
OrderItem {
sku: string (format "SKU-{4 chữ}{4 số}")
productName: string (tên sản phẩm thời trang tiếng Việt, thực tế)
price: number (đơn vị VND, khoảng 50000-2000000, chia hết cho 1000)
qty: number (1-5)
}
QUAN TRỌNG: subtotal, shippingFee, total phải được tính đúng theo công thức,
không tự bịa số. Kiểm tra lại phép tính trước khi xuất kết quả.
Nhấn mạnh yêu cầu "tính đúng theo công thức" ở cuối prompt vì LLM có xu hướng sinh số ngẫu nhiên cho các field tổng nếu không được nhắc lại — đây là lỗi thường gặp nhất khi sinh dữ liệu có ràng buộc tính toán.
Sinh Dữ Liệu Cho Testing Đa Ngôn Ngữ / Localization
Với các ứng dụng hỗ trợ nhiều locale, AI đặc biệt hữu ích vì nó biết các đặc thù văn hoá/ngôn ngữ mà Faker chuẩn không cover tốt: tên có ký tự đặc biệt, format ngày/giờ/tiền tệ theo từng vùng, chuỗi RTL (right-to-left) cho tiếng Ả Rập, chuỗi dài bất thường cho tiếng Đức, hoặc ký tự đa byte cho tiếng Nhật/Trung/Việt.
Sinh test data cho ô "Họ và tên" (input field, max 100 ký tự) để kiểm tra
localization, mỗi dòng là 1 test case, gồm:
1. Tên tiếng Việt đầy đủ dấu (ví dụ: Đặng Thị Ngọc Diễm)
2. Tên có ký tự đặc biệt hiếm gặp trong tiếng Việt (ví dụ chứa "Ữ", "Ẫ")
3. Tên tiếng Nhật viết bằng Kanji + Hiragana hỗn hợp
4. Tên tiếng Ả Rập (RTL) để kiểm tra hiển thị và bidi
5. Tên tiếng Đức có ký tự ß, ü, ö
6. Tên chứa emoji (invalid case — kỳ vọng bị reject)
7. Tên đúng 100 ký tự (boundary trên)
8. Tên 101 ký tự (boundary vượt, kỳ vọng bị reject hoặc bị cắt)
Với mỗi dòng, nêu rõ input string và kết quả mong đợi (accept/reject/truncate).
Sinh Trực Tiếp File Dữ Liệu CSV/JSON
Khi cần import dữ liệu hàng loạt cho môi trường test (seed database, hoặc test bulk-upload feature), yêu cầu AI xuất trực tiếp nội dung file thay vì mô tả bằng lời:
Sinh nội dung file CSV "products_seed.csv" với header:
sku,name,category,price,stock,is_active
Yêu cầu 30 dòng sản phẩm ngành điện tử, trong đó:
- 2 dòng có price = 0 (kỳ vọng validation lỗi khi import)
- 2 dòng có stock âm (-5, -1) (kỳ vọng validation lỗi)
- 1 dòng sku bị trùng với dòng khác trong file (kỳ vọng lỗi duplicate)
- 1 dòng thiếu category (empty string) (kỳ vọng lỗi required field)
- Còn lại là dữ liệu hợp lệ, đa dạng category (laptop, phone, tablet, accessory)
Chỉ xuất nội dung CSV thuần, không markdown code fence, không giải thích.
Kết quả AI trả về là nội dung CSV thuần, dán thẳng vào file là chạy được (dưới đây là trích đoạn rút gọn, bản đầy đủ có 30 dòng):
sku,name,category,price,stock,is_active
SKU-LAP0001,Laptop Dell Vostro 15,laptop,15000000,20,true
SKU-PHN0002,Samsung Galaxy A55,phone,7500000,35,true
SKU-TAB0003,iPad Gen 9 64GB,tablet,9800000,12,true
SKU-ACC0004,Tai nghe Bluetooth Sony WH-CH520,accessory,1200000,50,true
SKU-LAP0005,Laptop Asus Vivobook 14,laptop,0,15,true
SKU-PHN0006,Xiaomi Redmi Note 13,phone,4200000,-5,true
SKU-TAB0007,Samsung Galaxy Tab A9,tablet,5600000,-1,true
SKU-ACC0008,Sạc dự phòng Anker 10000mAh,,890000,40,true
SKU-LAP0001,Laptop HP 240 G9 (trùng SKU với dòng 2),laptop,11000000,8,true
Cách làm này cho ra ngay file có thể cat > products_seed.csv và import thẳng vào script test — không cần xử lý hậu kỳ. Lưu ý dòng cuối cố tình trùng sku với dòng thứ hai (SKU-LAP0001) để test case "duplicate sku" có dữ liệu thật để trỏ vào, và dòng SKU-ACC0004-tiếp-theo bỏ trống category để test case "thiếu required field" cũng có dữ liệu cụ thể — đừng chỉ mô tả case bằng lời trong đầu, hãy luôn kiểm tra file CSV AI xuất ra có đúng đủ số case đã yêu cầu hay không trước khi import.
Làm Thế Nào Để Prompt AI Sinh Dữ Liệu Biên và Edge Case Theo Tư Duy Tấn Công?
Đây là phần giá trị nhất của AI trong test data generation: sinh dữ liệu theo boundary value analysis một cách hệ thống và đầy đủ hơn con người thường nghĩ ra khi làm tay, vì con người dễ bỏ sót các trường hợp biên "kép" (hai điều kiện biên giao nhau).
Mẹo: Khi prompt AI sinh boundary data, luôn yêu cầu nó trả về ở dạng bảng có cột "lý do chọn giá trị này" (rationale). Việc buộc model giải thích lý do chọn từng giá trị boundary giúp bạn phát hiện ngay khi nó hiểu sai ràng buộc nghiệp vụ (ví dụ nó tưởng field là inclusive nhưng thực ra exclusive) — sửa ngay tại bước sinh dữ liệu rẻ hơn nhiều so với phát hiện lúc chạy test fail.
Pattern Prompt Đặc Tả Boundary (The Boundary Specification Prompt Pattern)
Pattern chuẩn để prompt boundary data gồm 4 phần: field, kiểu ràng buộc (inclusive/exclusive, min/max), các mốc cần test (min-1, min, min+1, max-1, max, max+1, và giá trị "hợp lý giữa khoảng"), và định dạng output kỳ vọng.
Field: "quantity" trong form đặt hàng.
Ràng buộc: integer, min = 1 (inclusive), max = 99 (inclusive).
Business rule bổ sung: nếu quantity > 50 thì cần approval từ manager (không tự
động submit).
Sinh bảng test case boundary value analysis đầy đủ cho field này, các cột:
| Giá trị input | Loại boundary | Kết quả mong đợi | Lý do |
Bắt buộc phải bao gồm các mốc: 0, 1, 2, 49, 50, 51, 98, 99, 100, giá trị âm,
giá trị không phải số nguyên (1.5), giá trị null/empty, giá trị vượt kiểu số
nguyên (ví dụ 999999999999).
Sinh Dữ Liệu Biên Cho Kiểu Số (Numeric)
Với các field số có nhiều lớp ràng buộc (kiểu dữ liệu, business rule, giới hạn hệ thống), nên tách prompt để AI xử lý từng lớp rõ ràng — tránh nó chỉ tập trung vào business rule mà quên giới hạn kỹ thuật của kiểu dữ liệu.
Sinh 15 test case boundary cho field "transferAmount" (số tiền chuyển khoản,
đơn vị VND, kiểu BIGINT ở backend):
Lớp 1 - Business rule: min 10,000 VND, max 500,000,000 VND/lần chuyển
Lớp 2 - Ràng buộc UI: input dạng text, format hiển thị có dấu phân cách nghìn
Lớp 3 - Ràng buộc kỹ thuật: BIGINT overflow (> 9,223,372,036,854,775,807),
số âm, số thập phân (VND không có phần lẻ), số 0, ký tự không phải số
trong input text ("10.000a"), input rỗng, input toàn khoảng trắng
Xuất dạng JSON array các object { input: string, expectedResult: string,
category: string }.
Kết quả mẫu AI có thể trả:
[
{ "input": "9999", "expectedResult": "reject - dưới mức tối thiểu 10,000", "category": "business-min" },
{ "input": "10000", "expectedResult": "accept - đúng mức tối thiểu", "category": "business-min-boundary" },
{ "input": "10001", "expectedResult": "accept", "category": "business-min-boundary" },
{ "input": "500000000", "expectedResult": "accept - đúng mức tối đa", "category": "business-max-boundary" },
{ "input": "500000001", "expectedResult": "reject - vượt mức tối đa", "category": "business-max" },
{ "input": "9223372036854775808", "expectedResult": "reject/overflow - vượt BIGINT", "category": "technical-overflow" },
{ "input": "-10000", "expectedResult": "reject - số âm không hợp lệ", "category": "technical-invalid" },
{ "input": "10000.5", "expectedResult": "reject - VND không có phần thập phân", "category": "business-format" },
{ "input": "10.000a", "expectedResult": "reject - ký tự không hợp lệ", "category": "technical-invalid" },
{ "input": "", "expectedResult": "reject - required field", "category": "technical-empty" }
]
Dữ Liệu Biên Về Ngày Giờ
Ngày giờ là nơi dễ sinh bug nhất vì có quá nhiều "biên ẩn": chuyển năm nhuận, chuyển múi giờ, DST (daylight saving time), giới hạn Unix timestamp, và ràng buộc nghiệp vụ (ngày sinh không được ở tương lai, ngày hết hạn hợp đồng không được trước ngày ký).
Sinh 12 test case boundary cho field "contractEndDate" trong hệ thống HR.
Ràng buộc nghiệp vụ: contractEndDate phải sau contractStartDate ít nhất 1 ngày,
và không được quá 5 năm sau contractStartDate.
contractStartDate mẫu: 2026-07-09.
Bắt buộc bao gồm các case:
- Đúng contractStartDate (0 ngày sau) -> reject
- contractStartDate + 1 ngày -> accept (boundary dưới)
- contractStartDate + 5 năm chính xác -> accept (boundary trên, kể cả năm nhuận
nằm trong khoảng)
- contractStartDate + 5 năm + 1 ngày -> reject
- Ngày 29/2 của năm nhuận làm mốc start hoặc end (kiểm tra logic +5 năm khi
năm đích không nhuận, ví dụ 2028-02-29 + 5 năm = 2033 không nhuận)
- Ngày ở định dạng timezone khác nhau cho cùng "ngày dương lịch" (00:00 UTC+7
vs 17:00 UTC ngày trước) — kiểm tra có bị lệch 1 ngày do quy đổi timezone
sai không
- Timestamp âm (trước 1970-01-01)
- Ngày vượt giới hạn 32-bit Unix timestamp (sau 2038-01-19)
Output: bảng markdown với cột Input, Boundary Type, Expected Result.
Dữ Liệu Biên Cho File Upload
Với chức năng upload file, boundary không chỉ nằm ở kích thước mà còn ở định dạng, tên file, và nội dung file. AI có thể sinh kế hoạch test đầy đủ, nhưng bạn vẫn cần script thật để tạo ra các file nhị phân đó — đây là chỗ kết hợp AI-generated script với thực thi thật rất hiệu quả.
Viết một Node.js script dùng `fs` để tạo ra các file test cho chức năng
upload avatar (giới hạn: JPEG/PNG, tối đa 2MB, kích thước ảnh tối đa 4096x4096px):
1. valid-1mb.jpg - file JPEG hợp lệ, đúng 1MB
2. exactly-2mb.jpg - file JPEG đúng chính xác 2MB (2 * 1024 * 1024 bytes)
3. over-2mb-by-1byte.jpg - file JPEG 2MB + 1 byte (kỳ vọng reject)
4. zero-byte.jpg - file rỗng 0 byte, đúng extension .jpg
5. fake-extension.jpg - file thực chất là nội dung .exe nhưng đổi extension
thành .jpg (kiểm tra magic-byte validation, không chỉ check đuôi file)
6. valid.png - PNG hợp lệ
7. huge-dimension.png - ảnh PNG hợp lệ về size (dưới 2MB) nhưng kích thước
pixel 8000x8000 (vượt giới hạn 4096x4096)
8. corrupted.jpg - file có magic byte JPEG đúng nhưng phần còn lại là dữ liệu
ngẫu nhiên (kiểm tra xử lý file hỏng, không crash server)
In ra đường dẫn từng file đã tạo sau khi chạy xong.
Làm Thế Nào Để Dùng AI Sinh Data Factory và Fixture Tái Sử Dụng?
Sinh dữ liệu rời rạc từng lần chỉ giải quyết vấn đề ngắn hạn. Giá trị bền vững hơn là biến AI thành công cụ viết data factory — module code sinh dữ liệu có tham số, override được, và dùng lại xuyên suốt test suite.
Mẹo: Khi yêu cầu AI viết factory, luôn cung cấp interface/type hiện có của project (paste type definition thật, không mô tả bằng lời) — nếu không AI sẽ tự đoán field name và bạn sẽ phải sửa tay hàng loạt chỗ dùng sai tên field.
Sinh TypeScript Test Data Factory
Đây là interface User hiện tại của project (từ src/types/user.ts):
interface User {
id: string;
email: string;
fullName: string;
role: 'admin' | 'member' | 'viewer';
createdAt: string; // ISO 8601
isActive: boolean;
subscription: {
plan: 'free' | 'pro' | 'enterprise';
expiresAt: string | null;
};
}
Viết một factory function `buildUser(overrides?: Partial<User>): User` bằng
TypeScript, dùng @faker-js/faker cho các field không được override, đảm bảo:
- email luôn unique giữa các lần gọi (dùng faker + timestamp/uuid suffix)
- role default là 'member'
- subscription.plan default 'free' và expiresAt null nếu plan là free,
nếu override plan thành 'pro'/'enterprise' mà không override expiresAt
thì tự set expiresAt là 30 ngày sau createdAt
- kèm theo `buildUserList(count: number, overrides?: Partial<User>): User[]`
Viết đầy đủ code, có comment giải thích logic subscription.
Bản đầu AI sinh ra có ...overrides tràn ở cuối object return — sau khi review kỹ đã bỏ đi vì gây bug âm thầm (giải thích ngay dưới code). Đây là bản đã sửa đúng:
import { faker } from '@faker-js/faker';
import type { User } from '../types/user';
export function buildUser(overrides: Partial<User> = {}): User {
const createdAt = overrides.createdAt ?? faker.date.recent({ days: 90 }).toISOString();
const plan = overrides.subscription?.plan ?? 'free';
// Business rule: paid plans get a 30-day expiry from createdAt by default,
// free plan has no expiry unless explicitly overridden.
const defaultExpiresAt =
plan === 'free'
? null
: new Date(new Date(createdAt).getTime() + 30 * 24 * 60 * 60 * 1000).toISOString();
return {
id: overrides.id ?? faker.string.uuid(),
email: overrides.email ?? `${faker.internet.userName()}.${Date.now()}@example.com`,
fullName: overrides.fullName ?? faker.person.fullName(),
role: overrides.role ?? 'member',
createdAt,
isActive: overrides.isActive ?? true,
subscription: {
plan,
expiresAt: overrides.subscription?.expiresAt ?? defaultExpiresAt,
},
};
}
export function buildUserList(count: number, overrides: Partial<User> = {}): User[] {
return Array.from({ length: count }, () => buildUser(overrides));
}
Lý do bỏ ...overrides ở cuối: nếu spread nguyên overrides sau khi đã build xong object trả về, field subscription (nested object) sẽ bị đè nguyên khối bởi overrides.subscription — ví dụ gọi buildUser({ subscription: { plan: 'pro' } }) thì expiresAt vừa tính cẩn thận ở defaultExpiresAt sẽ biến mất, trả về undefined thay vì mốc 30 ngày sau createdAt như business rule yêu cầu. Đây là lỗi rất hay gặp khi AI viết factory cho object có field lồng nhau: model tưởng spread cuối chỉ "bổ sung thêm" field top-level, nhưng thực chất nó ghi đè toàn bộ object con. Luôn review kỹ mỗi khi thấy AI đặt ...overrides sau các field đã được tính toán logic riêng — cách an toàn nhất là liệt kê rõ từng field như trên và không dùng spread tràn.
Sinh Pytest Fixture Cho API Test
Project dùng pytest + requests, base_url lấy từ biến môi trường API_BASE_URL.
Viết pytest fixture trong conftest.py:
1. `test_customer` (function-scoped): tạo customer mới qua POST /api/customers
với data factory ngẫu nhiên, yield customer object (đã có id trả về từ API),
sau test tự động DELETE /api/customers/{id} để cleanup, kể cả khi test fail
(dùng try/finally hoặc yield trong try block)
2. `authenticated_session` (session-scoped): login 1 lần bằng account test cố
định (đọc từ TEST_ACCOUNT_EMAIL, TEST_ACCOUNT_PASSWORD trong env), trả về
requests.Session() đã có Authorization header, dùng lại cho cả session
3. `random_order_payload` (function factory fixture, nhận tham số qua closure):
trả về hàm cho phép gọi random_order_payload(item_count=3, customer_id=...)
để sinh payload đơn hàng ngẫu nhiên hợp lệ
Đảm bảo cleanup luôn chạy dù test assertion fail giữa đường.
import os
import uuid
import pytest
import requests
BASE_URL = os.environ["API_BASE_URL"]
@pytest.fixture
def test_customer():
payload = {
"email": f"qa.factory.{uuid.uuid4().hex[:8]}@example.com",
"fullName": "QA Factory Customer",
"phone": "0900000000",
}
resp = requests.post(f"{BASE_URL}/api/customers", json=payload)
resp.raise_for_status()
customer = resp.json()
try:
yield customer
finally:
requests.delete(f"{BASE_URL}/api/customers/{customer['id']}")
@pytest.fixture(scope="session")
def authenticated_session():
session = requests.Session()
resp = session.post(
f"{BASE_URL}/api/auth/login",
json={
"email": os.environ["TEST_ACCOUNT_EMAIL"],
"password": os.environ["TEST_ACCOUNT_PASSWORD"],
},
)
resp.raise_for_status()
token = resp.json()["accessToken"]
session.headers.update({"Authorization": f"Bearer {token}"})
return session
@pytest.fixture
def random_order_payload():
def _build(item_count: int = 2, customer_id: str = None):
return {
"customerId": customer_id or f"CUST-{uuid.uuid4().hex[:6]}",
"items": [
{"sku": f"SKU-{uuid.uuid4().hex[:8].upper()}", "qty": i + 1, "price": 100000 * (i + 1)}
for i in range(item_count)
],
}
return _build
Sinh Playwright Fixture Kèm Database Seeding
Project Playwright + PostgreSQL (pg pool có sẵn ở src/test/db.ts, export
`pool: Pool`). Viết custom fixture trong fixtures.ts, mở rộng base test của
Playwright, thêm fixture `seededProject`:
- Trước test: insert trực tiếp vào DB (bảng projects) 1 project với owner_id
cố định từ biến môi trường TEST_OWNER_ID, trả về project row vừa insert
(đọc lại bằng SELECT để lấy đúng id do DB generate)
- Sau test: DELETE project đó và các bảng con liên quan (project_members,
project_tasks) theo project_id, theo đúng thứ tự tránh lỗi foreign key
- Fixture phải dùng worker-scoped pool connection, không mở connection mới
mỗi test
import { test as base } from '@playwright/test';
import { pool } from '../src/test/db';
type ProjectRow = { id: string; name: string; owner_id: string };
export const test = base.extend<{ seededProject: ProjectRow }>({
seededProject: async ({}, use) => {
const ownerId = process.env.TEST_OWNER_ID!;
const insertResult = await pool.query(
`INSERT INTO projects (name, owner_id) VALUES ($1, $2) RETURNING id, name, owner_id`,
[`E2E Seeded Project ${Date.now()}`, ownerId],
);
const project = insertResult.rows[0] as ProjectRow;
await use(project);
// Cleanup child tables first to satisfy FK constraints, then the project row.
await pool.query(`DELETE FROM project_tasks WHERE project_id = $1`, [project.id]);
await pool.query(`DELETE FROM project_members WHERE project_id = $1`, [project.id]);
await pool.query(`DELETE FROM projects WHERE id = $1`, [project.id]);
},
});
export { expect } from '@playwright/test';
Dữ Liệu Test Nào Tuyệt Đối Không Được Gửi Cho AI?
Đây là phần quan trọng nhất về mặt rủi ro của cả bài — vì lợi ích tốc độ của AI rất dễ khiến người dùng quên mất mình đang gửi dữ liệu thật ra ngoài hạ tầng công ty. Với các công cụ AI chạy trên cloud của bên thứ ba (ChatGPT web, Claude web, Copilot khi không dùng bản enterprise có cam kết không lưu data), bất cứ thứ gì bạn paste vào có thể bị log lại phía nhà cung cấp, và nếu đó là PII (personally identifiable information — thông tin định danh cá nhân) hoặc dữ liệu nhạy cảm, bạn có thể đang vi phạm chính sách bảo mật nội bộ hoặc quy định pháp luật (Nghị định 13/2023 về bảo vệ dữ liệu cá nhân tại Việt Nam, GDPR nếu công ty có khách hàng EU).
Mẹo: Đặt quy tắc "zero real data" ở cấp team, không phải cấp cá nhân — nghĩa là công cụ CI/lint nên tự động chặn việc paste dump database thật vào file prompt template được commit, thay vì trông cậy vào việc mỗi engineer tự nhớ. Một cách rẻ tiền: viết pre-commit hook grep các pattern email/số điện thoại/CCCD thật trong các file *.prompt.md trước khi cho commit.
Các Nhóm Dữ Liệu Không Bao Giờ Được Gửi
- PII trực tiếp: họ tên thật, số CCCD/CMND/passport, số điện thoại thật, địa chỉ nhà, email cá nhân thật của khách hàng hoặc nhân viên.
- Dữ liệu tài chính thật: số thẻ tín dụng, số tài khoản ngân hàng, mã CVV, lịch sử giao dịch thật — dù chỉ là ví dụ minh hoạ lấy từ log production.
- Secret và credential: API key, access token, password hash, private key, connection string có chứa mật khẩu.
- Dữ liệu y tế/sức khoẻ nếu domain là healthtech — thuộc nhóm dữ liệu nhạy cảm mức cao nhất theo hầu hết khung pháp lý.
- Toàn bộ response/log production thật dán nguyên văn để "làm ví dụ cho AI hiểu format" — kể cả khi bạn nghĩ chỉ cần cấu trúc, log thật luôn có khả năng chứa dữ liệu người dùng lẫn trong đó.
Cách Ẩn Danh Hoá Dữ Liệu Trước Khi Gửi
Nếu bạn cần AI hiểu cấu trúc thật của dữ liệu production (ví dụ để sinh test data cùng shape với response thật) nhưng không muốn gửi giá trị thật, hãy ẩn danh hoá trước bằng công cụ có sẵn, đừng tự tay xoá từng field bằng mắt (dễ sót).
Cách nhanh dùng jq để thay các field PII bằng giá trị giả trước khi paste:
cat production_sample.json | jq '
.[] |= (
.fullName = "REDACTED_NAME"
| .email = "redacted@example.com"
| .phone = "0900000000"
| .nationalId = "000000000000"
| .address = "REDACTED_ADDRESS"
)
' > anonymized_sample.json
Với dữ liệu phức tạp hơn, viết script Python để ẩn danh hoá có kiểm soát (giữ nguyên "shape" thống kê, chỉ thay giá trị nhạy cảm):
import json
import re
import uuid
PII_FIELDS = {"fullName", "email", "phone", "nationalId", "address"}
def anonymize(obj):
if isinstance(obj, dict):
result = {}
for key, value in obj.items():
if key in PII_FIELDS:
result[key] = f"REDACTED_{key.upper()}_{uuid.uuid4().hex[:6]}"
else:
result[key] = anonymize(value)
return result
if isinstance(obj, list):
return [anonymize(item) for item in obj]
return obj
with open("production_sample.json") as f:
data = json.load(f)
with open("anonymized_sample.json", "w") as f:
json.dump(anonymize(data), f, indent=2, ensure_ascii=False)
Sau khi có anonymized_sample.json, mới paste nội dung này vào prompt:
Đây là structure JSON thật của response API /api/orders (đã được ẩn danh hoá,
giá trị REDACTED_* không mang ý nghĩa thật):
<paste nội dung anonymized_sample.json>
Dựa trên cấu trúc này, sinh 20 bản ghi order test data mới với giá trị hợp lý,
giữ đúng shape và kiểu dữ liệu của từng field, nhưng dùng dữ liệu hoàn toàn
giả định, đa dạng.
Giải Pháp An Toàn Thay Thế Để Có Dữ Liệu Thực Tế
Thay vì lấy dữ liệu thật, có ba lựa chọn an toàn: (1) mô tả domain bằng lời càng chi tiết càng tốt và để AI tự sinh dữ liệu hợp lý (như các ví dụ ở phần đầu bài); (2) dùng chính schema/type definition của code (không chứa giá trị thật) làm input cho AI; (3) dùng công cụ ẩn danh hoá tự động có sẵn trong pipeline (ví dụ chạy synthetic data generation ngay trong staging database bằng tool nội bộ, không đưa dữ liệu ra khỏi mạng công ty, chỉ đưa mô tả kết quả cho AI nếu cần).
Với các công cụ AI có bản cam kết không lưu trữ/không train trên dữ liệu (enterprise tier của nhiều nhà cung cấp), rủi ro giảm đáng kể — nhưng vẫn nên áp dụng nguyên tắc "ẩn danh trước, gửi sau" làm mặc định, vì cấu hình enterprise có thể bị đổi hoặc áp dụng sai phạm vi tài khoản.
Prompt Chính Sách Cấp Tổ Chức
Nhiều team giải quyết vấn đề này bằng cách nhúng chính sách ngay vào system prompt hoặc custom instructions của công cụ AI mà cả team dùng, để không phụ thuộc vào việc từng người tự nhớ quy tắc:
Bạn là trợ lý AI hỗ trợ QA engineer sinh test data. Luôn tuân thủ:
1. Nếu người dùng paste dữ liệu có dấu hiệu là PII thật (họ tên đầy đủ kèm
số điện thoại/email/CCCD trông như dữ liệu thật, không phải ví dụ rõ ràng
là giả), hãy dừng lại và cảnh báo trước khi xử lý, đề nghị ẩn danh hoá.
2. Không bao giờ tự sinh ra số thẻ tín dụng có thể pass được thuật toán Luhn
thật trừ khi người dùng nói rõ đây chỉ dùng trong sandbox test của cổng
thanh toán (ví dụ test card của Stripe/VNPay).
3. Khi sinh dữ liệu mẫu minh hoạ cấu trúc, luôn dùng domain @example.com,
số điện thoại 090000000x, không dùng tên người có thật kể cả người nổi
tiếng.
Việc set policy ở cấp system prompt như trên biến AI thành một lớp phòng vệ thêm (defense-in-depth), không thay thế được việc training nhận thức bảo mật cho con người, nhưng giảm được nhiều trường hợp vô ý.