Vì sao sau 4 năm fullstack, tôi vẫn nghĩ như một tester
Bốn năm QA không biến mất khi tôi bắt đầu viết feature. Đây là cách nó vẫn hiện diện trong từng PR tôi mở.
Cuối năm 2016, tôi vừa tốt nghiệp Đại học Cần Thơ và vào FPT Software với vai trò manual tester cho một dự án .NET doanh nghiệp. Đến năm 2020 thì tôi đã chuyển sang viết code kiếm sống. Nhưng cái "DNA tester" thì không bao giờ rời khỏi tôi.
Đây là một bài viết nhỏ về lý do tôi biết ơn điều đó, và cách nó vẫn hiện diện trong mọi PR tôi mở năm 2026.
Bản năng đầu tiên vẫn là "cái gì làm cái này vỡ"
Khi đọc một spec, điều đầu não tôi làm không phải là xây cái này thế nào. Mà là input nào sẽ làm nó vỡ. Chuỗi rỗng. Số 0. Số âm. User refresh ngay giữa lúc submit. Mạng treo ở 99%. Lệch múi giờ 4 tiếng so với server. Nút bị double-click vì cái loading state quên disable.
Tôi không còn viết những thứ này ra giấy. Tôi phác chúng trong đầu khi vẫn đang đọc ticket, và chúng trở thành test plan ngầm tôi dựng feature xung quanh. Hầu hết thời gian, tôi không bao giờ viết file test thật cho chúng — feature ra lò đã tự bao chúng rồi, vì tôi đã thiết kế xung quanh phần thất bại trước khi thiết kế xung quanh happy path.
Tôi coi spec như một người kể chuyện không đáng tin
Bốn năm QA dạy tôi rằng spec chỉ là câu chuyện ai đó kể với chính mình về cách feature nên hoạt động. Gần như không bao giờ là toàn bộ câu chuyện.
Ở công ty cũ tôi từng ship một tính năng "gửi email mời" rất đơn giản. Spec nói: nhấn nút, gửi email tới địa chỉ đã nhập. Đơn giản. Hai ngày, ship.
Ba tuần sau nó nổ production. Không phải vì code sai, mà vì hai nhà tuyển
dụng cùng nhấn nút cho cùng một ứng viên gần như đồng thời, và audit log
có hai dòng sent_at với hai cái tên khác nhau. Spec không nói gì về
concurrency. Product manager không biết để hỏi.
Tư duy QA sẽ bắt được lỗi đó ngay từ ngày đầu. Hai nhà tuyển dụng, cùng ứng viên, đua nhau. Không phải edge case khôn ngoan gì cả — đó là test case thứ hai bạn viết nếu từng chạy manual test plan cùng một con người khác.
Giờ tôi đọc mọi spec hai lần. Một lần cho cái nó nói. Một lần cho cái nó quên không nói. Những case bị quên là nơi bug thực sự sinh sống.
Phần mô tả PR là một test plan ngụy trang
Đây là thói quen mà đồng nghiệp review code của tôi hay nhắc tới nhất.
Khi tôi mở pull request, phần mô tả luôn có mục "How I tested this", và nó không bao giờ trống. Nó trông kiểu:
## How I tested this
- ✅ Happy path: tạo invitation, người nhận nhận được email
- ✅ Trường email rỗng — nút submit vẫn disabled
- ✅ Email sai format — báo lỗi inline, không cho submit
- ✅ Người nhận đã được mời rồi — server trả 409, UI hiện
toast "đã mời trước đó"
- ✅ Hai recruiter cùng nhấn submit trong 1s — chỉ một row trong
DB, người còn lại cũng nhận toast "đã mời"
- ⚠️ SMTP timeout — thông báo cho user "thử lại", log lại để
debug. Không retry tự động; xem #4123.
- ❌ Chưa test: throttling khi tải > 50 req/s (Lokesh đang làm)Dòng cuối là dòng tôi tự hào nhất. Chưa test là thành thật. Chưa test là thông tin reviewer có thể hành động. Một PR description giả vờ rằng mọi thứ đã được test là đang lừa người review.
Format này không cao siêu. Nó là một checklist với ba trạng thái — chạy được, chạy được kèm caveat, chưa kịp làm. Tôi học được ở FPT, nơi mỗi test case đều có status, và tôi không bao giờ bỏ thói quen đó.
"Definition of done" là trách nhiệm của engineer
Ở nhiều team, "definition of done" là một thứ giấy tờ do project manager viết và không ai code đụng vào đọc. Tôi không đồng ý với cách nhìn đó.
Nếu tôi là engineer ship nó, tôi là người trả lời được câu cái này có thật sự chạy không. Product manager có thể nói thành công trông như thế nào từ góc nhìn khách hàng; chỉ tôi mới nói được code có thực sự sinh ra kết quả đó trong những failure mode mà họ không kịp nghĩ tới.
Vì vậy, định nghĩa "done" của riêng tôi có 4 phần:
- Happy path chạy và một người trong team demo được mà không cần tôi ngồi cạnh.
- Hai failure mode rõ ràng nhất được xử lý — thường là lỗi mạng và lỗi quyền.
- Rollback là một nút bấm — feature flag, env var, hoặc
git revertgọn gàng không kéo theo migration schema. - Người kế tiếp debug được lúc 11h đêm mà không cần DM tôi. Nghĩa là
có log, có error message gọi tên đúng thao tác đang fail, và một
payload error có cấu trúc — không chỉ
Error: 500.
Không gì trong đó là "best practice" hào nhoáng. Đó chỉ là bốn thứ tôi ước có ai đó đưa cho mình trong tháng đầu ở Remolution.
Cái lớn nhất phải "học lại": test không phải là mục tiêu
Đây là điều tôi đã phải chủ động quên đi khi chuyển sang dev: ship một test suite xanh không giống ship một sản phẩm chạy được.
Sáu tháng đầu làm dev, tôi over-test. Mỗi helper có unit test. Mỗi component có snapshot. CI chạy 11 phút và bug tôi ship vẫn y hệt bug đồng nghiệp ship, vì tôi đang test cái dễ test, không phải cái dễ vỡ.
Bước chuyển đến khi tôi nghiêng sang ít test hơn nhưng integration test tốt hơn — những test đi qua đúng path user thật, gồm cả gọi mạng, ghi database. Chúng chậm viết hơn. Hay vỡ hơn. Cũng bắt được bug thật.
Vì sao tôi nghĩ điều này quan trọng với bất kỳ ai chuyển sang dev
Nếu bạn vào nghề qua một "cửa phụ" — QA, support, design, ops, project management — bạn đang mang theo một thứ mà phần lớn dev tốt nghiệp CS không có: ký ức về hệ thống vỡ trên user thật.
Ký ức đó là tính năng, không phải bug. Nó xuất hiện dưới dạng sự thận trọng đúng chỗ, dưới dạng câu hỏi trong sprint planning ngăn được hai tuần làm lại, dưới dạng PR description nêu rõ phần chưa hoạt động.
Nếu bạn là người tuyển dụng: pipeline tester-sang-dev là một trong những phễu nhân tài bị đánh giá thấp nhất ngành. Chúng tôi đã từng ở đầu nhận của code dở.
Còn nếu bạn là tôi, chín năm rồi và đã rẽ ba lần: cứ tiếp tục mở file như một tester. Cứ tiếp tục hỏi cái gì làm cái này vỡ trước khi hỏi xây nó như thế nào. Ngày tôi ngừng làm điều đó là ngày tôi nên lo lắng.
Đây là bài đầu trong một loạt bài tôi đang viết về cung đường nghề nghiệp từ QA → fullstack → bất cứ gì tiếp theo. Nếu muốn nhận các bài sau qua email, nút đăng ký nằm cuối mỗi trang.