Mở đầu — vì sao bài này quan trọng
Có một sự thật phũ phàng mà ít ai nói cho bạn nghe khi bắt đầu học automation testing: phần lớn các dự án automation trên thế giới thất bại. Không phải vì công cụ tồi, không phải vì kỹ sư kém, mà vì họ rơi vào những cái bẫy (pitfalls) rất kinh điển, lặp đi lặp lại từ team này sang team khác, từ năm này qua năm khác.
Nếu bạn đã đi qua các bài trước — locators, wait strategies, Page Object Model, CI/CD, flaky test, test maintenance — thì bài này giống như một buổi tổng kết kinh nghiệm chiến trường. Ở các bài kỹ thuật, bạn học cách làm đúng một kỹ thuật cụ thể. Ở bài này, tôi muốn bạn lùi lại một bước, nhìn toàn cảnh, và nhận diện những sai lầm mang tính chiến lược và tư duy — những sai lầm khiến cả một suite automation trị giá hàng nghìn giờ công trở nên vô dụng.
Tôi đã chứng kiến nhiều team QA ở Việt Nam đầu tư 6 tháng xây dựng framework, có hơn 2.000 test case tự động, để rồi 1 năm sau âm thầm gỡ bỏ vì "chạy mãi không tin được". Số tiền và thời gian đó lẽ ra có thể dùng để tạo ra giá trị thật. Bài học hôm nay chính là để bạn không lặp lại bi kịch đó. Đây không phải lý thuyết suông — đây là những cái bẫy mà chính bạn, một ngày nào đó, sẽ đứng ngay trước miệng nó.
Khái niệm cốt lõi
"Pitfall" trong automation không phải là một lỗi code đơn lẻ. Nó là một mô hình sai lầm có hệ thống — thường bắt nguồn từ quyết định sai ngay từ đầu, rồi tích lũy dần cho đến khi hậu quả bùng nổ. Chúng ta sẽ đi qua sáu cái bẫy phổ biến và nguy hiểm nhất.
1. Over-automation — Tự động hóa mọi thứ
Đây là cái bẫy số một, và cũng là cái bẫy hấp dẫn nhất với người mới. Khi bạn vừa biết viết automation, bạn có cảm giác mọi test case đều xứng đáng được automate. Bạn automate cả những kịch bản chỉ chạy một lần duy nhất trước khi release, automate cả những màn hình đang thay đổi liên tục mỗi sprint.
Triệu chứng: 100% test case được yêu cầu phải tự động, bất kể tần suất dùng hay độ ổn định của tính năng.
Hệ quả: 70–80% thời gian của team đổ vào việc bảo trì suite (sửa test hỏng), chỉ còn 20% để thực sự phát hiện bug mới. Suite phình to nhưng giá trị thực giảm dần. Đây chính là lý do bài "Khi nào nên automate — ROI & candidates" tồn tại: không phải cái gì cũng đáng automate.
2. Testing implementation thay vì behavior
Bẫy này tinh vi hơn. Bạn viết test bám chặt vào cách hệ thống được xây dựng thay vì hệ thống làm được gì. Ví dụ: assert vào một chuỗi HTML cụ thể, vào thứ tự các phần tử trong DOM, vào tên biến nội bộ. Chỉ cần dev refactor mà không đổi hành vi, test của bạn đã đỏ hàng loạt.
Test tốt phải trả lời câu hỏi "người dùng có làm được việc họ cần không?", chứ không phải "code có được viết đúng theo cách tôi tưởng tượng không?".
3. Không có wait strategy đúng — dùng sleep cứng
Đây là cội nguồn của phần lớn flaky test. Thread.sleep(3000) hoặc time.sleep(3) rải khắp code là dấu hiệu của một suite sắp sụp đổ. Máy chạy nhanh thì lãng phí thời gian, máy chạy chậm hoặc mạng lag thì test fail dù app hoàn toàn đúng. (Bạn đã học explicit/fluent wait ở bài trước — đây là nơi kiến thức đó cứu mạng bạn.)
4. Test phụ thuộc lẫn nhau (test interdependency)
Test B chỉ chạy được nếu test A đã chạy trước và để lại dữ liệu. Nghe có vẻ tiện, nhưng đây là quả bom hẹn giờ. Khi bạn muốn chạy song song (parallel), khi bạn muốn chạy lại một test lẻ để debug, hoặc khi test A fail — toàn bộ chuỗi sau sụp đổ theo. Test tốt phải độc lập, tự chuẩn bị dữ liệu của chính nó, tự dọn dẹp sau khi chạy.
5. Bỏ qua flaky test — "chạy lại là qua"
Cái bẫy giết chết niềm tin. Khi một test lúc đỏ lúc xanh, phản ứng tự nhiên là thêm cơ chế retry hoặc bảo nhau "chạy lại đi, chắc do mạng". Nhưng mỗi lần bạn tặc lưỡi bỏ qua, bạn đang huấn luyện cả team ngừng tin vào kết quả test. Đến một ngày, khi test đỏ báo đúng một bug thật, không ai buồn nhìn nữa. Suite automation lúc đó chính thức chết lâm sàng.
6. Không coi test code là code thật
Test code cũng cần review, cần refactor, cần đặt tên rõ ràng, cần tránh trùng lặp. Rất nhiều team viết test theo kiểu "copy-paste cho nhanh", không áp dụng POM, không tách data khỏi logic. Kết quả: đổi một nút bấm phải sửa 40 chỗ. Test code tệ thì việc bảo trì đắt gấp nhiều lần giá trị nó mang lại.
Tình huống thực tế
Ví dụ 1 — Sàn thương mại điện tử "over-automate" đến kiệt sức
Một công ty thương mại điện tử tầm trung ở TP.HCM (tạm gọi là ShopViet, khoảng 40 kỹ sư) quyết định "phủ automation 100%" cho website. Trong 5 tháng, team QA gồm 4 người xây được 1.800 test case UI end-to-end chạy qua Selenium, bao gồm cả những màn hình khuyến mãi Tết — vốn chỉ tồn tại vài tuần rồi bị gỡ.
Đến mùa cao điểm, đội marketing thay đổi giao diện trang chủ và luồng thanh toán gần như mỗi tuần để chạy campaign. Mỗi lần đổi, khoảng 300–400 test đỏ. Hai trong bốn kỹ sư QA phải dành toàn bộ thời gian chỉ để sửa test hỏng, không còn ai kiểm thử thủ công cho các tính năng mới. Trớ trêu thay, một bug nghiêm trọng ở tính năng ví điện tử — thứ ít khi đổi và lẽ ra phải được test kỹ — lại lọt lên production vì không ai có thời gian nhìn tới.
Bài học: Automation phải nhắm vào các luồng ổn định, chạy thường xuyên, giá trị cao (đăng nhập, thêm giỏ hàng, thanh toán core). Những màn hình đổi liên tục hoặc dùng một lần thì test thủ công rẻ và nhanh hơn nhiều. Sau khi cắt suite từ 1.800 xuống còn 600 test tập trung, thời gian bảo trì giảm 65% và team lấy lại được năng lực kiểm thử thật.
Ví dụ 2 — Fintech và cái chết bởi sleep cứng
Một startup fintech ở Hà Nội tích hợp cổng thanh toán, có suite khoảng 500 test API + UI chạy trên Jenkins. Để "cho chắc", các bạn rải Thread.sleep(5000) sau mỗi thao tác gọi cổng thanh toán sandbox.
Hai vấn đề xảy ra cùng lúc. Thứ nhất, mỗi lần chạy full suite mất hơn 2 tiếng — đội dev không chịu chờ, bắt đầu merge code mà không đợi CI xanh. Thứ hai, khi sandbox phản hồi chậm hơn 5 giây (chuyện thường xuyên vào giờ cao điểm), test vẫn fail dù giao dịch thực tế thành công. Tỷ lệ flaky lên tới gần 15%.
Team "chữa cháy" bằng cách bọc mọi test trong cơ chế retry 3 lần. Kết quả: thời gian chạy tăng lên gần 4 tiếng, và một bug xử lý timeout thật của cổng thanh toán bị retry che mất — chỉ phát hiện khi khách hàng khiếu nại mất tiền.
Bài học: Sleep cứng và retry mù quáng là hai cái bẫy cộng hưởng. Sau khi thay toàn bộ bằng explicit wait (chờ đúng điều kiện cụ thể như "nút xác nhận hiện ra" thay vì "chờ 5 giây"), thời gian chạy giảm xuống 35 phút, flaky rate còn dưới 1%, và bug timeout được lộ ra ngay.
Ví dụ 3 — Test phụ thuộc lẫn nhau ở một team outsourcing
Một team gia công phần mềm làm dự án cho khách hàng Singapore viết bộ test đăng ký người dùng. Test test_login chỉ chạy được nếu test_register đã tạo tài khoản user_demo@test.com trước đó. Trong nhiều tháng, mọi thứ ổn vì test chạy tuần tự theo đúng thứ tự alphabet.
Khi khách hàng yêu cầu rút ngắn thời gian CI, team bật chế độ chạy song song 4 luồng. Ngay lập tức khoảng 40% test đỏ ngẫu nhiên: bốn luồng cùng cố tạo user_demo@test.com, gây lỗi trùng email; test_login chạy trước test_register ở luồng khác nên không tìm thấy tài khoản. Debug mất gần hai tuần, suýt làm trễ deadline release.
Bài học: Mỗi test phải tự sinh dữ liệu duy nhất của riêng mình (ví dụ email kèm timestamp user_1719_482@test.com) và tự dọn dẹp. Độc lập không phải là thứ "làm cho đẹp" — nó là điều kiện bắt buộc để có thể chạy song song và mở rộng.
Hướng dẫn từng bước
Đây là quy trình thực tế để bạn phòng và chữa các pitfall trong dự án của mình.
Bước 1 — Trước khi viết bất kỳ test nào, hỏi "test này có đáng automate không?". Áp dụng bộ ba tiêu chí: (1) luồng này có chạy lặp lại nhiều lần không? (2) nó có ổn định, ít thay đổi không? (3) chi phí test thủ công có cao không? Nếu cả ba đều "không", đừng automate.
Bước 2 — Viết test bám vào hành vi, không bám vào implementation. Assert theo góc nhìn người dùng: "sau khi bấm thanh toán, thấy thông báo thành công". Tránh assert vào chuỗi HTML nội bộ hay ID sinh động. Ưu tiên locator ổn định (như đã học ở bài Locators).
Bước 3 — Xóa sạch mọi sleep cứng. Thay bằng explicit/fluent wait chờ đúng điều kiện. Đặt một quy tắc trong team: bất kỳ PR nào chứa Thread.sleep hay time.sleep đều bị từ chối review.
Bước 4 — Đảm bảo mỗi test độc lập. Setup dữ liệu trong @BeforeEach/fixture, teardown trong @AfterEach. Sinh dữ liệu duy nhất. Test phải chạy được khi đứng một mình và khi chạy song song.
Bước 5 — Thiết lập chính sách zero-tolerance với flaky test. Khi một test flaky xuất hiện, đưa nó vào danh sách theo dõi, gắn tag cách ly (quarantine) và phải có người chịu trách nhiệm sửa trong sprint đó — không được phép để nó "chạy lại là qua" mãi mãi.
Bước 6 — Đối xử với test code như production code. Bắt buộc code review cho test. Áp dụng POM và data-driven để tránh trùng lặp. Refactor định kỳ.
Bước 7 — Đo lường sức khỏe suite hàng tháng. Theo dõi ba chỉ số: thời gian chạy, tỷ lệ pass ổn định (không tính retry), và số bug thật mà automation bắt được. Nếu số bug thật gần bằng 0 trong nhiều tháng, suite của bạn đang chỉ tốn tiền chứ không tạo giá trị.
Lỗi thường gặp & mẹo
"Coverage 100% nghĩa là chất lượng tốt". Sai. Coverage cao mà toàn test yếu, test trùng lặp, test không assert đúng điều quan trọng thì con số đó vô nghĩa. Hãy đo bằng số bug bắt được, không phải phần trăm dòng code chạy qua.
Automate quá sớm, khi UI/API còn chưa ổn định. Nếu tính năng đang thay đổi mỗi ngày, mọi test bạn viết hôm nay sẽ hỏng ngày mai. Mẹo: chờ tính năng "đóng băng" tương đối rồi mới automate; giai đoạn đầu ưu tiên exploratory testing thủ công.
Dùng retry để giấu flaky. Retry hợp lý ở tầng hạ tầng (ví dụ lỗi mạng thoáng qua) thì được, nhưng dùng retry để che test bất ổn là tự lừa dối. Mẹo: log lại mọi lần retry và review chúng — nếu một test cần retry thường xuyên, đó là bug cần sửa, không phải chuyện bình thường.
Hard-code dữ liệu môi trường (URL, tài khoản, token) ngay trong test. Khi đổi môi trường staging sang production hay đổi mật khẩu, cả suite gãy. Mẹo: tách hết vào file config/biến môi trường.
Không ai làm chủ suite. Khi automation là "việc của mọi người" thì thành "việc của không ai". Mẹo: chỉ định một người/nhóm owner chịu trách nhiệm sức khỏe tổng thể của suite.
Chạy automation nhưng không ai xem kết quả. Test đỏ mà không có cảnh báo tới đúng người thì bằng không. Mẹo: tích hợp thông báo tự động (Slack/Telegram/email) và biến "CI đỏ" thành việc phải xử lý ngay, không được merge đè.
Bài tập thực hành
- Audit suite hiện tại (hoặc một dự án mẫu trên GitHub). Tìm và đếm số lần xuất hiện
Thread.sleep/time.sleep. Viết lại ít nhất 3 chỗ thành explicit wait. Ghi lại thời gian chạy trước và sau.
- Săn test phụ thuộc. Lấy một bộ test bất kỳ, xáo trộn thứ tự chạy (nhiều framework có option
random order). Ghi lại những test nào fail khi đổi thứ tự — đó chính là các test có interdependency cần sửa.
- Xây bảng quyết định automation. Với 10 test case của một tính năng thật (ví dụ luồng đăng nhập + đăng ký), cho điểm từng case theo ba tiêu chí ở Bước 1 (tần suất, độ ổn định, chi phí thủ công) trên thang 1–3. Quyết định case nào nên automate, case nào để thủ công. Giải thích lựa chọn.
- Phân tích flaky. Chạy một suite nhỏ 20 lần liên tiếp. Ghi lại test nào không cho kết quả ổn định 100%. Với mỗi test flaky, viết một câu chẩn đoán nguyên nhân gốc (wait sai? dữ liệu chung? phụ thuộc thứ tự?).
- Viết checklist review test code gồm 8–10 mục cho team của bạn, dựa trên các pitfall trong bài. Dùng nó để review một PR test thật.
Tóm tắt
Automation thất bại hiếm khi vì công cụ — nó thất bại vì tư duy. Sáu cái bẫy lớn nhất bạn cần khắc cốt ghi tâm: over-automation (automate cả thứ không đáng), test bám implementation thay vì behavior, sleep cứng gây flaky, test phụ thuộc lẫn nhau, bỏ qua flaky làm mất niềm tin, và coi thường chất lượng test code.
Nguyên tắc xuyên suốt: automation là một khoản đầu tư, không phải một mục tiêu. Mỗi test bạn viết là một tài sản phải bảo trì suốt đời. Hãy automate ít nhưng đúng, chạy nhanh và đáng tin, hơn là automate nhiều mà không ai dám tin kết quả. Ba con số cần theo dõi mãi mãi: thời gian chạy, độ ổn định thật (không tính retry), và số bug thật bắt được.
Khi bạn đứng trước quyết định "có nên automate cái này không?", hãy nhớ những ví dụ hôm nay — ShopViet kiệt sức vì phủ 100%, fintech mất tiền vì sleep và retry, team outsourcing suýt trễ deadline vì test phụ thuộc nhau. Tránh được sáu cái bẫy này, bạn đã đi trước phần lớn kỹ sư automation ngoài kia rồi.