Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vừa phát hiện một lỗi nghiêm trọng: nút "Thanh toán" trên website bán hàng của công ty không hoạt động trên trình duyệt Safari. Bạn báo lỗi cho đội phát triển. Rồi... im lặng. Ba ngày sau bạn hỏi lại, dev bảo "tôi tưởng lỗi đó đã được ai đó xử lý rồi". Một tuần sau, lỗi vẫn còn nguyên, khách hàng iPhone không mua được hàng, và sếp gọi bạn lên hỏi "tại sao tester lại để lọt lỗi này?".
Vấn đề ở đây không phải là bạn không tìm ra lỗi — bạn đã tìm ra rồi. Vấn đề là con lỗi đó bị "lạc" trong quy trình. Không ai biết rõ nó đang ở trạng thái nào, ai đang chịu trách nhiệm, và bước tiếp theo là gì.
Đây chính là lý do mọi tester chuyên nghiệp phải nắm vững vòng đời của defect (Defect Lifecycle) và quy trình xử lý bug (bug workflow). Một defect không phải là một sự kiện xảy ra một lần rồi thôi — nó là một thực thể có "vòng đời", đi qua nhiều trạng thái từ lúc sinh ra cho đến lúc "chết" (đóng). Hiểu rõ vòng đời này giúp bạn biết chính xác con lỗi đang ở đâu, ai giữ quả bóng, và khi nào nó thực sự được giải quyết.
Trong bài này, chúng ta sẽ đi sâu vào sơ đồ trạng thái chuẩn theo ISTQB, ý nghĩa từng trạng thái, các nhánh rẽ đặc biệt (như lỗi bị từ chối, lỗi trùng lặp, lỗi tái xuất hiện), và cách vận hành một quy trình bug workflow mượt mà trong đội của bạn. Lưu ý: bài này tập trung vào dòng chảy trạng thái của defect. Còn cách viết một bug report cho hay, phân biệt Severity với Priority, hay quy trình triage chi tiết — những chủ đề đó có bài riêng, tôi sẽ không lấn sân ở đây.
Khái niệm cốt lõi
Defect lifecycle là gì?
Defect lifecycle (còn gọi là bug lifecycle) là chuỗi các trạng thái (status) mà một defect trải qua trong suốt thời gian tồn tại của nó, kể từ khi được phát hiện và ghi nhận cho đến khi được đóng lại vĩnh viễn. Mỗi lần defect chuyển từ trạng thái này sang trạng thái khác, ta gọi là một state transition (chuyển trạng thái), và mỗi lần chuyển thường đi kèm việc bàn giao trách nhiệm giữa các vai trò (tester, developer, trưởng nhóm...).
Điểm mấu chốt bạn cần nhớ: tại mỗi thời điểm, một defect chỉ ở đúng một trạng thái, và trạng thái đó cho biết ai đang cầm quả bóng. Nếu defect ở trạng thái "Assigned to Dev", nghĩa là dev đang chịu trách nhiệm. Nếu ở "Ready for Retest", nghĩa là tester đang cầm bóng. Nhờ vậy không ai đùn đẩy hay bỏ sót.
Sơ đồ trạng thái chuẩn theo ISTQB
ISTQB mô tả một vòng đời defect điển hình gồm các trạng thái chính sau. Đây là luồng "hạnh phúc" (happy path) khi mọi thứ diễn ra suôn sẻ:
NEW → ASSIGNED → OPEN (In Progress) → FIXED → RETEST → CLOSED
Diễn giải từng trạng thái:
- NEW (Mới): Tester vừa phát hiện lỗi và tạo bug report. Lỗi chưa được ai xem xét hay xác nhận. Đây là điểm khởi đầu của mọi defect.
- ASSIGNED (Đã phân công): Trưởng nhóm dev hoặc quản lý dự án đã xem qua, xác nhận đây là lỗi hợp lệ và giao cho một developer cụ thể xử lý. Quả bóng giờ nằm ở phía dev.
- OPEN / IN PROGRESS (Đang xử lý): Developer đã bắt đầu phân tích và sửa lỗi. Trạng thái này cho cả đội biết "có người đang thực sự làm việc trên con lỗi này".
- FIXED (Đã sửa): Developer tuyên bố đã sửa xong và code đã được đưa vào môi trường test. Tuy nhiên — và đây là điểm rất nhiều tester non kinh nghiệm hiểu sai — "Fixed" KHÔNG có nghĩa là lỗi đã hết. Nó chỉ có nghĩa là dev nghĩ rằng mình đã sửa. Chưa ai kiểm chứng.
- RETEST (Kiểm thử lại): Tester nhận lại defect và tiến hành kiểm tra lại đúng kịch bản đã gây ra lỗi ban đầu, trên bản build mới. Đây là bước quyết định.
- CLOSED (Đã đóng): Sau khi retest, tester xác nhận lỗi thực sự đã hết. Defect được đóng vĩnh viễn. Vòng đời kết thúc.
Các nhánh rẽ quan trọng (không phải happy path)
Thực tế đời sống của defect hiếm khi đi thẳng một mạch. Có nhiều nhánh rẽ mà bạn buộc phải hiểu:
REOPENED (Mở lại): Sau khi retest, tester phát hiện lỗi vẫn còn hoặc dev sửa chưa triệt để. Thay vì đóng, tester chuyển defect về trạng thái REOPENED và trả lại cho dev. Từ đây nó quay lại vòng ASSIGNED → OPEN → FIXED → RETEST một lần nữa. Một defect có thể bị reopen nhiều lần — và số lần reopen là một chỉ báo sức khỏe rất đáng chú ý (tôi sẽ nói thêm ở phần mẹo).
REJECTED / INVALID (Bị từ chối): Dev hoặc trưởng nhóm xem xét và kết luận đây không phải lỗi. Ví dụ: tester hiểu sai yêu cầu, hoặc phần mềm đang hoạt động đúng như thiết kế. Defect chuyển sang REJECTED. Nếu tester đồng ý thì đóng luôn; nếu không đồng ý, hai bên cần trao đổi lại (đây là lúc dễ xảy ra xung đột nhất).
DUPLICATE (Trùng lặp): Lỗi này đã có người báo trước đó rồi. Defect được đánh dấu DUPLICATE và liên kết tới defect gốc, rồi đóng lại. Việc gộp defect trùng giúp tránh nhiều dev cùng sửa một chỗ.
DEFERRED / POSTPONED (Hoãn lại): Lỗi có thật, nhưng đội quyết định chưa sửa ngay — thường vì độ ưu tiên thấp, hoặc không kịp trong bản release này. Defect được "để dành" cho một phiên bản sau. Nó không bị đóng mà nằm chờ.
NOT A BUG / WORKING AS DESIGNED: Gần giống Rejected, nhưng nhấn mạnh rằng hành vi đang đúng với đặc tả — vấn đề nằm ở kỳ vọng của tester chứ không ở sản phẩm.
Khi vẽ đầy đủ, sơ đồ trông như thế này:
┌─────────────────────────────┐
↓ │
NEW → ASSIGNED → OPEN → FIXED → RETEST → CLOSED │
│ │ │ │
│ ↓ ↓ (lỗi còn) │
│ REJECTED REOPENED ───────────┘
│ DUPLICATE
│ DEFERRED
└──→ (đóng nếu không hợp lệ)
Vì sao cần một workflow chuẩn hóa?
Bạn có thể hỏi: tại sao phải phức tạp thế, cứ báo lỗi rồi sửa lỗi là xong? Câu trả lời nằm ở ba giá trị:
- Truy vết trách nhiệm (accountability): Ở mỗi trạng thái luôn có một người chịu trách nhiệm rõ ràng. Không ai "lạc trôi".
- Đo lường được (measurability): Nhờ trạng thái, ta thống kê được: bao nhiêu bug đang mở, bao nhiêu đã fix chờ retest, tỷ lệ reopen là bao nhiêu. Đây là dữ liệu vàng cho quản lý chất lượng.
- Giao tiếp chung một ngôn ngữ: Khi cả đội dùng chung bộ trạng thái, một câu "bug #234 đang Reopened" mang ý nghĩa chính xác với tất cả mọi người, không cần giải thích dài dòng.
Tình huống thực tế
Tình huống 1: Sàn thương mại điện tử và con bug "chết đi sống lại"
Một công ty thương mại điện tử tại TP.HCM (tạm gọi là ShopViet) chuẩn bị ra mắt tính năng áp mã giảm giá. Chị Lan, tester, phát hiện: khi khách nhập mã "TET2026" thì hệ thống trừ tiền hai lần. Chị tạo bug #512, trạng thái NEW.
Trưởng nhóm dev xác nhận đây là lỗi thật, chuyển sang ASSIGNED, giao cho bạn Minh. Minh sửa, chuyển FIXED sau nửa ngày. Chị Lan retest — lỗi trừ hai lần đã hết, nhưng giờ mã giảm giá lại không được áp dụng gì cả. Fix một chỗ, hỏng chỗ khác. Chị chuyển bug về REOPENED kèm ghi chú chi tiết.
Minh sửa lần hai, FIXED. Chị Lan retest lần ba — lần này ổn. Bug #512 chuyển CLOSED. Tổng cộng con bug này đã đi qua vòng lặp hai lần.
Bài học: "Fixed" chỉ là lời hứa của dev, "Closed" mới là sự thật do tester xác nhận. Nếu chị Lan tin lời Minh mà đóng bug ngay từ lần đầu (không retest), thì đến ngày ra mắt, khách sẽ không áp được mã "TET2026" — đúng dịp cao điểm bán hàng Tết. Trạng thái REOPENED chính là "cái phanh" cứu cả chiến dịch.
Tình huống 2: Ngân hàng số và bug bị "Deferred" một cách có chủ đích
Tại một ngân hàng số ở Đông Nam Á, đội QA phát hiện: khi người dùng đổi ảnh đại diện sang ảnh dung lượng trên 8MB, app hiển thị thông báo lỗi bằng... tiếng Anh thay vì tiếng Việt. Rõ ràng là một defect. Bug #88 được tạo, trạng thái NEW, rồi ASSIGNED.
Tuy nhiên bản release này chỉ còn 3 ngày nữa là lên production, và trọng tâm là tính năng chuyển tiền liên ngân hàng. Trưởng nhóm quyết định: đây là lỗi cosmetic (bề mặt), độ ưu tiên thấp, không ảnh hưởng giao dịch. Bug #88 chuyển sang trạng thái DEFERRED, hẹn xử lý ở sprint sau.
Ba tuần sau, ở sprint mới, bug #88 được kéo ra khỏi trạng thái Deferred, đưa lại vào ASSIGNED và xử lý dứt điểm.
Bài học: DEFERRED không phải là "phủi tay" hay "giấu bug dưới thảm". Đó là một quyết định quản lý có ghi nhận, có theo dõi. Con bug vẫn tồn tại trong hệ thống, không bị lãng quên, và sẽ được lôi ra đúng thời điểm. Một tester tốt phải phân biệt được: bug bị Deferred (còn sống, chờ xử lý) khác hoàn toàn với bug bị Rejected (bị coi là không hợp lệ). Đừng để ai lạm dụng Deferred như một cách "chôn" bug vĩnh viễn — hãy luôn rà soát danh sách Deferred ở đầu mỗi sprint.
Tình huống 3: Startup và cuộc tranh cãi "Rejected"
Một startup giao đồ ăn có tester mới tên Huy. Huy báo bug: "Khi lọc nhà hàng theo khoảng cách, kết quả không sắp xếp từ gần đến xa". Bug chuyển ASSIGNED. Dev xem xong bèn chuyển sang REJECTED với lý do "Working as designed — bộ lọc mặc định sắp theo độ phổ biến, không phải khoảng cách".
Huy suýt bỏ qua vì thấy dev là người kỳ cựu. Nhưng cậu mở lại tài liệu đặc tả (spec) và thấy dòng: "Khi người dùng chọn tiêu chí 'Gần nhất', kết quả PHẢI sắp xếp tăng dần theo khoảng cách". Cậu chụp lại spec, đính kèm và chuyển bug về REOPENED kèm bằng chứng.
Lần này dev thừa nhận đã đọc sót yêu cầu. Bug được ASSIGNED lại, sửa và CLOSED.
Bài học: Trạng thái REJECTED không phải là "án tử" cho một defect. Khi bạn tin mình đúng, hãy dùng bằng chứng (spec, thiết kế, yêu cầu) để đưa bug trở lại luồng. Workflow chuẩn cho phép sự phản biện có căn cứ — đó là điểm mạnh của nó, chứ không phải để tester "ngậm bồ hòn làm ngọt". Đồng thời, mọi lần chuyển trạng thái nên kèm ghi chú/bằng chứng, để cuộc tranh luận dựa trên dữ liệu chứ không phải cảm tính hay thâm niên.
Hướng dẫn từng bước
Đây là cách vận hành vòng đời defect trên thực tế, từ góc nhìn của một tester:
Bước 1 — Tạo defect ở trạng thái NEW. Ngay khi phát hiện lỗi và đã tái hiện được, tạo bug report trên công cụ quản lý (Jira, Azure DevOps, Redmine, Trello...). Đặt trạng thái mặc định NEW. Đảm bảo lỗi tái hiện được — đừng báo lỗi bạn chỉ thấy một lần và không lặp lại được.
Bước 2 — Theo dõi chuyển sang ASSIGNED. Trưởng nhóm/PM sẽ xem và giao dev. Nếu sau một khoảng thời gian hợp lý bug vẫn nằm im ở NEW, hãy chủ động nhắc trong buổi họp hoặc trên kênh chat. Đừng để bug "ngủ đông" ở NEW.
Bước 3 — Chờ trong khi bug ở OPEN/FIXED. Giai đoạn này quả bóng ở phía dev. Việc của bạn là chuẩn bị sẵn kịch bản retest và dữ liệu test, để khi bug chuyển FIXED bạn có thể kiểm tra ngay, không làm nghẽn dòng chảy.
Bước 4 — Retest nghiêm túc khi bug ở FIXED. Nhận bản build có chứa bản vá. Thực hiện đúng các bước tái hiện lỗi ban đầu. Kiểm tra cả những chức năng liên quan xung quanh (vì fix có thể gây tác dụng phụ, như tình huống ShopViet ở trên). Đây là lúc bạn không được lười.
Bước 5 — Quyết định CLOSED hay REOPENED.
- Nếu lỗi đã hết và không phát sinh vấn đề mới → chuyển CLOSED, ghi rõ đã retest trên build/version nào.
- Nếu lỗi còn hoặc phát sinh vấn đề mới → chuyển REOPENED, ghi chú chi tiết những gì bạn quan sát được, đính kèm ảnh chụp/video nếu có.
Bước 7 — Ghi chú ở MỌI lần chuyển trạng thái. Đây là thói quen phân biệt tester chuyên nghiệp với tester nghiệp dư. Mỗi transition đều để lại dấu vết: ai chuyển, khi nào, vì sao. Nhờ đó lịch sử con bug rõ ràng như một cuốn nhật ký.
Lỗi thường gặp & mẹo
Lỗi 1 — Coi "Fixed" là "hết lỗi". Đây là sai lầm phổ biến nhất. Chừng nào tester chưa retest và chuyển CLOSED, con bug vẫn chưa được xác nhận là hết. Nhiều đội "dính đòn" khi báo cáo với sếp rằng "đã fix 50 bug" trong khi thực chất 50 bug đó mới chỉ ở trạng thái Fixed, chưa qua kiểm chứng.
Lỗi 2 — Đóng bug quá sớm để "cho đẹp số liệu". Áp lực deadline khiến một số đội đóng bug vội. Điều này chỉ đẩy vấn đề ra production và làm mất niềm tin. Chỉ đóng khi thực sự đã retest.
Lỗi 3 — Không dùng trạng thái REOPENED, mà tạo bug mới. Khi lỗi cũ tái xuất, nhiều bạn tạo một bug hoàn toàn mới. Hậu quả: mất liên kết lịch sử, không thấy được rằng dev đã sửa hụt bao nhiêu lần. Hãy reopen bug cũ để giữ nguyên "hồ sơ bệnh án" của con lỗi.
Lỗi 4 — Lạm dụng DEFERRED để chôn bug. Như đã nói, Deferred phải đi kèm cam kết rà soát lại. Nếu không, danh sách Deferred trở thành nghĩa địa bug bị lãng quên.
Mẹo 1 — Theo dõi chỉ số Reopen Rate. Nếu một bug bị reopen 3-4 lần, đó là tín hiệu đỏ: hoặc bug report chưa rõ ràng, hoặc dev chưa hiểu đúng gốc rễ vấn đề, hoặc bản thân vấn đề phức tạp hơn ta tưởng. Hãy ngồi lại trao đổi trực tiếp thay vì cứ đá qua đá lại trên công cụ.
Mẹo 2 — Chuẩn hóa bộ trạng thái cho cả đội ngay từ đầu dự án. Đừng để nửa đội dùng "Fixed", nửa kia dùng "Resolved", "Done" lẫn lộn. Thống nhất một sơ đồ workflow duy nhất và dán nó lên tường (hoặc pin trên kênh chat).
Mẹo 3 — Luôn kèm version/build khi chuyển FIXED và CLOSED. "Đã fix ở build 2.3.1" giúp mọi người biết bản vá nằm ở đâu, tránh retest nhầm trên build cũ.
Bài tập thực hành
Bài 1 — Vẽ lại sơ đồ. Không nhìn tài liệu, hãy tự vẽ sơ đồ vòng đời defect đầy đủ bao gồm cả các nhánh REOPENED, REJECTED, DUPLICATE, DEFERRED. Đối chiếu lại với bài học và tìm điểm bạn còn thiếu.
Bài 2 — Truy vết trạng thái. Cho tình huống: Bug #700 được tạo, dev sửa, tester retest thấy còn lỗi, dev sửa lại, tester retest thấy hết. Hãy liệt kê đầy đủ chuỗi trạng thái mà bug #700 đã đi qua theo đúng thứ tự.
Bài 3 — Phân loại quyết định. Với mỗi trường hợp sau, hãy xác định trạng thái phù hợp nhất: (a) Lỗi có thật nhưng lãnh đạo quyết định để sprint sau; (b) Lỗi này đã có người báo tuần trước; (c) Phần mềm hoạt động đúng spec, tester hiểu nhầm; (d) Dev sửa xong nhưng tester chưa kiểm tra.
Bài 4 — Đóng vai. Bạn retest và thấy dev đã đóng bug của bạn mà lỗi vẫn còn. Hãy viết một đoạn ghi chú (comment) chuyển bug về REOPENED sao cho lịch sự, rõ ràng, có bằng chứng và không gây căng thẳng với dev.
Tóm tắt
- Defect có vòng đời: nó đi qua chuỗi trạng thái từ NEW đến CLOSED, và tại mỗi thời điểm chỉ ở đúng một trạng thái — cho biết ai đang chịu trách nhiệm.
- Happy path chuẩn ISTQB: NEW → ASSIGNED → OPEN → FIXED → RETEST → CLOSED.
- Các nhánh rẽ quan trọng: REOPENED (lỗi còn sau retest), REJECTED (không phải lỗi), DUPLICATE (trùng), DEFERRED (hoãn xử lý có chủ đích).
- "Fixed" chỉ là lời hứa của dev; "Closed" mới là sự thật do tester xác nhận qua retest. Đừng bao giờ nhầm lẫn hai điều này.
- Ghi chú ở mọi lần chuyển trạng thái và luôn kèm version/build — đó là thói quen của tester chuyên nghiệp.
- Theo dõi Reopen Rate và danh sách Deferred để giữ cho quy trình lành mạnh, không có bug nào bị bỏ quên hay đá qua đá lại vô ích.