Mở đầu — vì sao bài này quan trọng
Có một sự thật ít ai nói thẳng với bạn khi mới vào nghề automation: phần lớn các test tự động thất bại không phải vì logic kiểm thử sai, mà vì dữ liệu test bị hỏng, trùng lặp, hết hạn hoặc bị người khác chỉnh sửa. Bạn viết một test đăng nhập tuyệt đẹp với tài khoản alice@test.com, chạy ngon ngày đầu, rồi tuần sau đồng nghiệp xoá tài khoản đó khỏi database staging — thế là toàn bộ suite đỏ lòm, và bạn mất nửa buổi sáng để nhận ra nguyên nhân chẳng liên quan gì đến code sản phẩm.
Test data — dữ liệu đầu vào cho các bài test — là nền móng thầm lặng của cả hệ thống automation. Một chiến lược dữ liệu tốt giúp test độc lập, lặp lại được (repeatable) và chạy song song được. Một chiến lược tồi biến suite của bạn thành quả bom hẹn giờ: flaky, phụ thuộc lẫn nhau, và không ai dám sửa.
Bài này tập trung riêng vào câu hỏi: làm sao tạo ra và quản lý dữ liệu test một cách có chiến lược? Chúng ta sẽ không bàn về cách viết assertion hay cách chọn locator (đó là các bài khác), mà bàn về bốn chiến lược sinh dữ liệu, khi nào dùng cái nào, và cách tránh những cái bẫy khiến dữ liệu trở thành gánh nặng bảo trì. Đây là kỹ năng phân biệt một SDET biết viết test với một SDET biết xây hệ thống test bền vững.
Khái niệm cốt lõi
Trước hết, hãy phân biệt bốn thuộc tính mà mọi chiến lược dữ liệu test đều phải cân nhắc:
- Deterministic (xác định): cùng input phải cho cùng kết quả. Dữ liệu ngẫu nhiên không kiểm soát sẽ phá vỡ điều này.
- Isolated (cô lập): dữ liệu của test A không được ảnh hưởng test B. Đây là điều kiện tiên quyết để chạy song song.
- Realistic (thực tế): dữ liệu phải giống production đủ để bắt được bug thật (ví dụ tên có dấu tiếng Việt, số điện thoại 10 số bắt đầu bằng 0).
- Maintainable (dễ bảo trì): khi schema thay đổi, chi phí cập nhật dữ liệu phải thấp.
1. Static fixtures — dữ liệu tĩnh commit vào repo
Đây là cách đơn giản nhất: bạn viết sẵn dữ liệu vào file JSON, CSV, YAML hoặc SQL rồi commit vào Git. Test đọc file này lên và dùng.
// fixtures/users.json
[
{ "id": 1, "email": "alice@test.local", "role": "customer", "name": "Nguyễn Thị Lan" },
{ "id": 2, "email": "bob@test.local", "role": "admin", "name": "Trần Văn Bình" }
]
Ưu điểm: dễ đọc, dễ review trong pull request, hoàn toàn deterministic, không cần chạy code để tạo. Nhược điểm: cứng nhắc. Khi schema thêm cột phone_number NOT NULL, bạn phải sửa tay mọi record. Và fixtures rất dễ bị "chết cứng" — dữ liệu không còn phản ánh cấu trúc thật của hệ thống nhưng test vẫn xanh vì nó chỉ so với chính nó.
Static fixtures hợp nhất cho dữ liệu tham chiếu ổn định: danh mục quốc gia, danh sách tỉnh thành, bảng phí cố định, các case biên đã biết trước (email không hợp lệ, chuỗi rỗng, chuỗi 256 ký tự).
2. Data builders / factories — sinh dữ liệu bằng code
Thay vì viết cứng, bạn dùng một hàm/lớp tạo object với giá trị mặc định hợp lý, và chỉ ghi đè (override) những trường mà test đó thực sự quan tâm. Đây là pattern Object Mother hoặc Test Data Builder.
factories.py
def make_user(**overrides):
defaults = {
"email": f"user_{uuid4().hex[:8]}@test.local",
"name": "Người Dùng Mẫu",
"role": "customer",
"is_active": True,
}
return {defaults, overrides}Trong test — chỉ nêu điều quan trọng
admin = make_user(role="admin")
locked = make_user(is_active=False)
Điểm mạnh cốt lõi: test đọc ra được ý định. Khi nhìn make_user(role="admin"), người đọc hiểu ngay test này quan tâm đến quyền admin, còn email hay tên là chi tiết không liên quan. Trong hệ sinh thái, các thư viện như factory_boy (Python), Factory Bot (Ruby), hoặc tự viết builder (Java) đều theo triết lý này. Đây là chiến lược được khuyến nghị mặc định cho phần lớn dự án vì cân bằng tốt giữa dễ đọc và dễ bảo trì.
3. Faker / dữ liệu ngẫu nhiên có kiểm soát
Khi cần khối lượng lớn dữ liệu "trông thật" — ví dụ 10.000 đơn hàng để test hiệu năng danh sách — bạn dùng thư viện Faker để sinh tên, địa chỉ, email, số điện thoại giả nhưng đúng định dạng.
from faker import Faker
fake = Faker("vi_VN") # locale tiếng Việtuser = {
"name": fake.name(), # "Lê Thị Hồng"
"phone": fake.phone_number(), # định dạng VN
"address": fake.address(),
}
Điểm mấu chốt phải nhớ: luôn cố định seed khi cần lặp lại (Faker.seed(1234)), nếu không mỗi lần chạy ra dữ liệu khác, và khi test đỏ bạn không thể tái hiện. Faker tuyệt vời cho khối lượng và đa dạng, nhưng đừng dùng nó cho các assertion cụ thể — bạn không nên viết test kỳ vọng "tên phải là X" khi X là ngẫu nhiên.
4. Database seeding / snapshot — gieo dữ liệu vào DB
Với các test end-to-end cần trạng thái hệ thống phức tạp (một user đã có 3 đơn hàng, 1 đơn đang giao), bạn seed thẳng vào database qua script SQL hoặc gọi API/service của ứng dụng. Có hai trường phái:
- Seed qua API/service layer: gọi đúng luồng nghiệp vụ để tạo dữ liệu. Chậm hơn nhưng dữ liệu luôn hợp lệ theo mọi ràng buộc business.
- Seed thẳng SQL: nhanh, nhưng dễ tạo ra trạng thái mà ứng dụng thật không bao giờ tạo ra (bỏ qua validation), dẫn đến test "xanh giả".
Tình huống thực tế
Ví dụ 1 — Tiki và cơn ác mộng "tài khoản dùng chung"
Một đội QA tại một sàn thương mại điện tử lớn (tạm gọi mô hình giống Tiki) ban đầu dùng khoảng 20 tài khoản test cố định, ghi cứng trong file test_accounts.json, cho toàn bộ 400 test E2E. Mọi thứ ổn cho đến khi họ bật chạy song song 8 luồng để rút thời gian suite từ 90 phút xuống 15 phút.
Thảm hoạ xảy ra: test A đang thêm sản phẩm vào giỏ của buyer01@test, thì test B cùng lúc xoá giỏ của chính tài khoản đó. Kết quả là tỷ lệ flaky vọt lên gần 18% — cứ 5-6 lần chạy lại đỏ một cách ngẫu nhiên, không liên quan đến bug thật.
Giải pháp họ áp dụng: chuyển sang factory sinh tài khoản mới cho mỗi test qua API đăng ký, mỗi email gắn UUID để đảm bảo duy nhất, và dọn dẹp sau test. Flaky rate rơi xuống dưới 1%.
Bài học: dữ liệu dùng chung là kẻ thù số một của chạy song song. Khi chuyển sang parallel, mỗi test phải "sở hữu" dữ liệu riêng của nó.
Ví dụ 2 — Ví điện tử và cái bẫy dấu tiếng Việt
Một đội QA tại một ví điện tử ở TP.HCM dùng Faker locale en_US để sinh tên người dùng cho hàng nghìn test. Suite luôn xanh mướt. Nhưng khi lên production, một loạt lỗi hiển thị xuất hiện: tên "Nguyễn Thị Ánh Tuyết" bị vỡ layout, email xác thực gửi ra "Xin ch??o" vì lỗi encoding UTF-8 ở tầng gửi mail.
Nguyên nhân: dữ liệu test toàn tên tiếng Anh không dấu, nên không bao giờ chạm tới đường đi của ký tự Unicode có dấu. Bug tồn tại trong code nhưng test không thể thấy vì dữ liệu quá "sạch".
Họ đổi sang Faker locale vi_VN và bổ sung một bộ fixtures tĩnh chứa các tên có dấu khó (Đặng, Phạm, Huỳnh, các nguyên âm ghép). Ngay lập tức 3 bug encoding bị phát hiện trong CI trước khi lên production.
Bài học: dữ liệu test phải realistic theo đúng ngữ cảnh người dùng. Với sản phẩm Việt Nam, dữ liệu không dấu là một lỗ hổng che giấu bug.
Ví dụ 3 — Fintech và snapshot production bị rò rỉ PII
Một công ty fintech muốn dữ liệu "thật nhất có thể" nên copy nguyên một snapshot database production về môi trường staging để test. Vài tháng sau, kiểm toán bảo mật phát hiện: số CMND/CCCD, số tài khoản ngân hàng thật của khách hàng đang nằm phơi bày trên staging, nơi có hàng chục dev và nhà thầu truy cập được. Đây là vi phạm nghiêm trọng về bảo vệ dữ liệu cá nhân.
Họ phải xây một pipeline masking/anonymization: dữ liệu production khi sao chép sẽ được che (số tài khoản thay bằng số giả cùng định dạng, tên thay bằng Faker, giữ nguyên phân bố thống kê để test hiệu năng vẫn đúng).
Bài học: dùng dữ liệu production trực tiếp là con dao hai lưỡi. Realistic thì có, nhưng rủi ro pháp lý và bảo mật thì khổng lồ. Luôn masking trước khi đưa ra khỏi môi trường production.
Hướng dẫn từng bước
Đây là quy trình xây dựng chiến lược dữ liệu test cho một dự án mới:
- Phân loại nhu cầu dữ liệu. Liệt kê các loại: dữ liệu tham chiếu ổn định (danh mục), dữ liệu giao dịch (thay đổi liên tục), dữ liệu biên (edge case). Mỗi loại hợp với chiến lược khác nhau.
- Mặc định chọn factory/builder. Với đa số test, viết một lớp factory cho mỗi entity chính (User, Order, Product). Đặt giá trị mặc định hợp lệ, cho phép override từng trường.
- Đảm bảo tính duy nhất. Với các trường unique (email, username, mã đơn), gắn UUID hoặc timestamp:
order_{uuid}. Không bao giờ hardcode giá trị unique dùng lại.
- Chọn cơ chế isolation. Ưu tiên transaction rollback nếu test chạm DB. Nếu không thể (test qua UI thật), thì mỗi test tự tạo và tự dọn dữ liệu của mình (setup/teardown).
- Cố định seed cho phần ngẫu nhiên. Khi dùng Faker, đặt seed cố định trong CI để lỗi tái hiện được. Log seed ra report để debug.
- Localize dữ liệu. Với sản phẩm VN, dùng locale
vi_VN, bổ sung fixtures tên có dấu, số điện thoại 0xxx, địa chỉ đúng cấu trúc phường/quận.
- Tách dữ liệu khỏi test. Đừng nhúng dữ liệu lớn vào file test. Để fixtures ở thư mục riêng, factory ở module riêng — test chỉ gọi, không chứa dữ liệu.
- Dọn dẹp (cleanup). Mỗi test phải để lại DB sạch như lúc bắt đầu. Dùng teardown hoặc rollback. Dữ liệu rác tích tụ là nguồn gốc của flaky về sau.
Lỗi thường gặp & mẹo
- Hardcode ID tự tăng. Viết
assert user.id == 5là mời gọi thảm hoạ, vì ID phụ thuộc thứ tự chạy. Luôn tham chiếu qua biến trả về từ factory, không đoán giá trị.
- Dùng chung một tài khoản cho mọi test. Như ví dụ Tiki, đây là nguyên nhân số một phá vỡ chạy song song. Mỗi test sở hữu dữ liệu riêng.
- Ngẫu nhiên không có seed.
randomhay Faker không cố định seed khiến test "khi xanh khi đỏ" mà không tái hiện được. Luôn cố định và log seed.
- Fixtures chết cứng. File JSON viết từ 2 năm trước không còn khớp schema, nhưng test vẫn xanh vì chỉ so với chính nó. Định kỳ rà soát, hoặc chuyển sang factory bám theo model thật.
- Seed SQL bỏ qua validation. Tạo trạng thái mà ứng dụng thật không thể tạo (đơn hàng có total âm) khiến bạn test một thứ không tồn tại. Ưu tiên seed qua API khi tính hợp lệ quan trọng.
- Quên dọn dẹp. Dữ liệu rác tích tụ làm chậm DB test và gây xung đột unique về sau. Teardown là bắt buộc, không phải tuỳ chọn.
- Mẹo — "một nguồn sự thật cho dữ liệu". Đặt toàn bộ logic tạo dữ liệu ở một nơi (module factories). Khi schema đổi, bạn chỉ sửa một chỗ thay vì lùng sục 300 file test.
- Mẹo — dữ liệu biên là fixtures, dữ liệu khối lượng là Faker. Case biên (email sai, chuỗi rỗng, ký tự đặc biệt) nên viết tay và tĩnh để rõ ý định; còn khi cần nghìn bản ghi thì để Faker lo.
Bài tập thực hành
- Viết một factory
make_ordercho một sàn TMĐT giả định. Có giá trị mặc định hợp lệ (status = "pending", total > 0, buyer email gắn UUID). Cho phép overridestatusvàtotal. Viết 3 test dùng nó: đơn chờ xử lý, đơn đã giao, đơn có tổng tiền bằng 0 (case biên).
- Chuyển đổi fixtures sang factory. Cho một file
users.jsoncứng 5 record, hãy viết lại thành factory sao cho mỗi lần gọi sinh email duy nhất. Chứng minh bạn có thể chạy cùng test 2 lần liên tiếp mà không xung đột unique.
- Kiểm chứng realistic. Dùng Faker
vi_VNsinh 100 tên người dùng, cố định seed. Kiểm tra có bao nhiêu tên chứa ký tự có dấu (Đ, ă, ơ, ...). So sánh nếu đổi sang localeen_US— thảo luận vì sao sự khác biệt này có thể che giấu bug encoding.
- Thiết kế isolation. Cho một suite 50 test E2E chạm cùng một database. Viết ra chiến lược của bạn để chạy song song 4 luồng an toàn: dùng transaction rollback hay dữ liệu riêng mỗi test? Nêu đánh đổi.
Tóm tắt
Test data không phải chuyện phụ — nó là nền móng quyết định suite của bạn bền vững hay là quả bom flaky. Bốn chiến lược chính: static fixtures (đơn giản, hợp dữ liệu tham chiếu ổn định và case biên), factories/builders (lựa chọn mặc định, cân bằng dễ đọc và dễ bảo trì), Faker (khối lượng lớn, dữ liệu đa dạng — nhớ cố định seed), và database seeding (trạng thái phức tạp cho E2E — ưu tiên isolation bằng rollback).
Ba nguyên tắc vàng cần khắc cốt: dữ liệu phải cô lập để chạy song song được, phải xác định để tái hiện lỗi, và phải thực tế theo ngữ cảnh — với sản phẩm Việt Nam nghĩa là có dấu tiếng Việt và định dạng bản địa. Cuối cùng, đừng quên dọn dẹp và đừng bao giờ đưa dữ liệu production thật ra ngoài mà chưa masking. Nắm vững chiến lược dữ liệu, bạn đã đặt được viên gạch nền cho một hệ thống automation thực sự đáng tin cậy.