DEV Community

Edge Case — Boundary Testing

Boundary testing: bug nằm ở rìa, không ở giữa

Phần lớn bug production không nằm ở "đường chính" (input bình thường, đã chạy ngàn lần) mà ở các biên: danh sách rỗng, đúng giới hạn (off-by-one), giá trị null/undefined, số 0 và số âm, chuỗi rỗng, ngày đầu/cuối tháng, trang cuối của phân trang, request đồng thời. Testing mindset là thói quen, khi nhìn một feature, tự hỏi "rìa của nó ở đâu" trước khi nghĩ tới đường chính. Bỏ qua biên là lý do một feature "chạy tốt khi demo" rồi vỡ khi gặp dữ liệu thật đa dạng.

Cơ chế hoạt động

Với mỗi input/điều kiện, biên là các giá trị ngay tại và quanh ngưỡng, cùng các trạng thái đặc biệt:

function paginate(items, page, pageSize) {
  return items.slice((page - 1) * pageSize, page * pageSize)
}
// đường chính: page=2, pageSize=10, items có 100 phần tử -> OK
// các biên cần test:
//   items = []           (rỗng)
//   page = 1             (trang đầu)
//   page vượt số trang   (trả rỗng hay lỗi?)
//   page = 0 / âm        (đầu vào không hợp lệ)
//   items.length không chia hết pageSize (trang cuối thiếu)
//   pageSize = 0         (chia/slice bất thường)
Enter fullscreen mode Exit fullscreen mode

Boundary value analysis: với một ngưỡng N, test N-1, N, N+1. Với tập hợp, test rỗng, một phần tử, và "đầy". Phần lớn off-by-one lộ ra đúng ở các điểm này.

Vấn đề gặp trong production

Failure mode: dữ liệu rỗng/null không xử lý. Code viết với giả định "luôn có dữ liệu" vỡ khi gặp rỗng:

const avg = scores.reduce((a, b) => a + b) / scores.length // scores=[] -> reduce throw, hoặc chia 0 -> NaN
const first = users[0].name // users=[] -> undefined.name -> crash
Enter fullscreen mode Exit fullscreen mode

Mảng rỗng, kết quả truy vấn không có hàng, field optional vắng — đây là nhóm biên hay bị bỏ và hay nổ nhất, vì test thủ công lúc dev thường có sẵn dữ liệu.

Failure mode: off-by-one ở giới hạn. Phân trang lệch một hàng, vòng lặp <= thay vì <, lấy substring sai một ký tự — những lỗi này không lộ với input giữa dải, chỉ ở đúng biên (trang cuối, phần tử cuối). Chúng âm thầm: kết quả "gần đúng" nên dễ lọt qua review và demo.

Failure mode: bỏ qua biên thời gian và đồng thời. Ngày cuối tháng/năm, đổi múi giờ, giây nhuận; và biên đồng thời — hai request cùng sửa một bản ghi, cùng đặt đơn cho sản phẩm cuối kho. Những biên này không thấy được khi test tuần tự một mình, nhưng xuất hiện chắc chắn dưới tải production.

Failure mode: test nhiều tốn effort — cần chọn lọc. Không thể test mọi tổ hợp; chi phí là thật. Tập trung vào biên có khả năng xảy ra và hậu quả lớn (rỗng, null, off-by-one ở phân trang/tính tiền, đồng thời trên dữ liệu quan trọng), bỏ qua tổ hợp phi thực tế. Một checklist biên cho mỗi feature đáng giá hơn cố phủ 100%.

Cách debug và monitor

Khi nhận một bug production, ghi lại nó như một test biên trước khi sửa — phần lớn bug là một biên bị bỏ, và test đó ngăn nó tái diễn. Lập checklist biên chung cho team (rỗng, null/undefined, 0/âm, off-by-one, biên thời gian, đồng thời) và soi mỗi feature qua nó trong review. Theo dõi log production cho các lỗi đặc trưng của biên: Cannot read ... of undefined (null chưa xử lý), NaN trong số liệu (chia 0/mảng rỗng), lỗi chỉ xảy ra cuối tháng/cuối trang. Property-based testing (sinh input ngẫu nhiên gồm cả ca biên) bổ sung tốt cho test viết tay khi muốn phủ rộng dải giá trị.

Tradeoff

Test biên kỹ bắt được phần lớn bug trước khi ra production — đầu tư rẻ so với một sự cố. Cái giá là effort: viết và bảo trì test biên tốn thời gian, và không thể phủ mọi tổ hợp. Quy tắc thực tế: ưu tiên biên theo xác suất xảy ra × hậu quả — rỗng/null/off-by-one ở các luồng quan trọng (tính tiền, phân trang, phân quyền) gần như luôn đáng; tổ hợp hiếm và hậu quả nhỏ thì bỏ. Dùng checklist biên làm công cụ nhất quán, ghi mỗi bug production thành test biên, và dùng property-based test để mở rộng dải mà không viết tay từng ca. Mục tiêu là phủ rủi ro, không phải phủ con số.

Câu hỏi phỏng vấn

Những edge case thường gặp nhất là gì, và làm sao quyết định test cái nào khi không thể test hết?

Các biên hay gặp và hay nổ nhất: dữ liệu rỗng (mảng/kết quả truy vấn không có hàng → reduce throw, chia 0 ra NaN, arr[0] undefined), null/undefined ở field optional, giá trị 0 và âm, off-by-one tại ngưỡng (phân trang lệch, <= thay <), biên thời gian (cuối tháng/năm, múi giờ), và biên đồng thời (hai request cùng sửa một bản ghi). Kỹ thuật cốt lõi là boundary value analysis: với ngưỡng N test N-1/N/N+1, với tập hợp test rỗng/một/đầy. Khi không thể test hết, ưu tiên theo xác suất xảy ra × hậu quả: rỗng/null/off-by-one và đồng thời trên các luồng quan trọng (tính tiền, phân quyền, phân trang) gần như luôn đáng; tổ hợp hiếm hậu quả nhỏ thì bỏ. Điểm ăn điểm: dùng checklist biên chung trong review, ghi mỗi bug production thành một test biên để chặn tái diễn, và dùng property-based testing để phủ rộng dải giá trị mà không viết tay từng ca.

Hands-on

Lấy một hàm thật có ngưỡng và tập hợp (ví dụ paginate, hoặc tính trung bình/tổng, hoặc áp giảm giá theo bậc) và liệt kê các biên của nó trước khi viết test: rỗng, một phần tử, đúng ngưỡng và ±1, 0/âm, vượt giới hạn. Viết test cho từng biên và quan sát những ca làm hàm throw/NaN/sai off-by-one mà đường chính không lộ, rồi sửa. Lấy một bug production gần đây (hoặc dựng một bug biên đồng thời: hai lần đặt đơn cho sản phẩm cuối kho) và viết test tái hiện trước khi vá. Cuối cùng thêm một property-based test sinh input ngẫu nhiên để xem nó tự tìm ra ca biên nào mà test viết tay bỏ sót.

Top comments (0)