Ở nhiều dự án, unit test là thứ phải viết cho đủ để qua được bước kiểm tra coverage trong CI. Người ta viết vội vài test case, chạy thấy xanh rồi merge. Nhưng test viết cho có và test viết vì hiểu giá trị của nó khác nhau rất xa, và sự khác biệt đó chỉ lộ ra khi dự án lớn dần.
Unit test thực chất là gì
Unit test kiểm tra một đơn vị code nhỏ nhất có thể tách riêng, thường là một hàm hoặc một phương thức, trong điều kiện cô lập với phần còn lại của hệ thống. Mục tiêu không phải chứng minh code "chạy được", mà là khẳng định code hoạt động đúng như kỳ vọng với từng loại đầu vào: trường hợp bình thường, trường hợp biên, và trường hợp lỗi.
Dấu hiệu của test viết cho có
Test viết cho có thường dễ nhận ra: chỉ kiểm tra "happy path" (đầu vào đẹp, kết quả đúng); không có assertion nào thực sự kiểm tra kết quả mà chỉ gọi hàm để tăng coverage; mock quá nhiều đến mức test chỉ đang kiểm tra chính các mock; hoặc tên test mơ hồ kiểu test1, testFunction khiến không ai biết test đang bảo vệ điều gì.
Coverage cao nhưng test kiểu này tạo ra cảm giác an toàn giả. Con số báo 90% nhưng lỗi thật vẫn lọt qua, vì không test nào thực sự kiểm tra hành vi quan trọng.
Giá trị thật của unit test
Giá trị lớn nhất của unit test không nằm ở việc bắt lỗi ngay lúc viết, mà ở tương lai. Thứ nhất, test cho phép refactor tự tin: khi sửa lại cấu trúc code, bộ test báo ngay nếu hành vi bị thay đổi ngoài ý muốn. Không có test, mỗi lần refactor là một lần đánh cược. Thứ hai, test là tài liệu sống: đọc test của một hàm thường cho biết cách dùng nó nhanh hơn đọc comment, và test không bao giờ bị lỗi thời vì nếu lỗi thời thì nó sẽ fail. Thứ ba, test phát hiện lỗi sớm nhất có thể, khi chi phí sửa còn thấp, thay vì để lỗi lọt đến môi trường staging hay production.
Một lợi ích ít người nhắc tới: code khó viết test thường là dấu hiệu code đang thiết kế chưa tốt, ví dụ một hàm làm quá nhiều việc hoặc phụ thuộc chặt vào quá nhiều thành phần khác. Việc viết test buộc người viết nhìn lại thiết kế của chính mình.
Viết test sao cho có ý nghĩa
Vài nguyên tắc đơn giản giúp test có giá trị thật: đặt tên test mô tả hành vi, ví dụ returns_zero_when_cart_is_empty thay vì testCart; mỗi test chỉ kiểm tra một hành vi; luôn có test cho trường hợp biên và trường hợp lỗi, không chỉ trường hợp đẹp; chỉ mock những phụ thuộc bên ngoài thực sự (database, API, thời gian hệ thống), không mock chính logic đang cần kiểm tra; và giữ test chạy nhanh để mọi người sẵn lòng chạy thường xuyên.
Khi bị bắt buộc viết test, nhiều người coi đó là thủ tục. Khi hiểu vì sao cần, test trở thành công cụ giúp chính người viết làm việc nhanh hơn và ngủ ngon hơn mỗi lần deploy.