Nguyen Le PhongNguyen Le Phong

TDD có đáng thời gian không?

Một bài nhìn lại thực tế về khi nào Test-Driven Development đáng với chi phí bỏ ra: TDD định hình design thế nào, chỗ nào làm team chậm lại, và cách dùng vòng red-green-refactor mà không biến nó thành giáo điều.

Một developer đang sửa dở một thay đổi nhỏ trong pricing thì câu hỏi xuất hiện ở review: test đâu? Căn phòng im đi theo một cách rất quen. Ai cũng đồng ý test là tốt. Ai cũng nhớ lần gần nhất một thay đổi đơn giản biến thành hai giờ mock, fixture và setup dễ vỡ. TDD nghe có trách nhiệm, nhưng trong một tuần delivery bận, câu hỏi thật vẫn còn đó: nó có đáng thời gian không?

Test-Driven Development thường được mô tả bằng vòng lặp: viết một failing test, viết lượng code nhỏ nhất để pass, rồi refactor trong khi test vẫn xanh. Red, green, refactor. Ý tưởng đủ đơn giản để nói trong một câu, nhưng giá trị không chỉ nằm ở việc sau đó có test. Giá trị nằm ở việc buộc engineer mô tả behavior trước khi quá gắn bó với implementation.

Thứ tự đó thay đổi cách code được thiết kế. Khi viết test trước, bạn gặp code từ bên ngoài. Bạn hỏi input nào được chấp nhận, output nào nên sinh ra, edge case nào quan trọng, và caller không cần biết điều gì. Test khó viết thường hé lộ design khó dùng. Nếu một rule nhỏ cần boot nửa application mới test được, code có thể đang quá coupled. Nếu test nào cũng cần năm mock, boundary có thể chưa rõ. TDD làm những chi phí đó hiện ra sớm hơn.

TDD thường đáng thời gian khi logic quan trọng, phức tạp, hoặc có khả năng thay đổi. Pricing rule, permission check, scheduling behavior, data transformation, validation, state machine và retry decision là những ứng viên tốt. Các khu vực này thường có đủ edge case để việc test sau trở thành bài tập trí nhớ. Viết test trước giúp team chậm lại đúng lúc cần chính xác nhất.

Nó cũng hữu ích trước refactoring. Một characterization test nhỏ có thể capture behavior hiện tại trước khi code được dọn. Sau đó team có thể đổi structure mà không phải đoán behavior có bị dịch chuyển không. Đây là một sức mạnh âm thầm của TDD: nó làm refactoring bớt cảm tính. Cuộc thảo luận chuyển từ code mới có cảm giác sạch hơn không sang behavior nhìn thấy có còn được bảo vệ không.

Nhưng TDD không miễn phí. Nó có thể làm exploration chậm lại khi team chưa hiểu vấn đề. Trong spike, prototype hoặc UI sketch, viết test trước đôi khi tạo nhiều ma sát hơn clarity. Có lúc bước đầu đúng là học, vứt bỏ một bản nháp, rồi quay lại với test khi shape đã rõ hơn. Xem TDD như rule cho mọi dòng code có thể biến một practice hữu ích thành một màn trình diễn hình thức.

TDD cũng đau khi test bám vào implementation detail thay vì behavior. Nếu refactor nào cũng làm test gãy trong khi feature vẫn chạy đúng, test không tạo safety. Nó đang đóng băng nhầm thứ. TDD tốt hỏi unit hứa điều gì, không phải private helper nào được gọi. Sự khác biệt này cần luyện tập, và đó là lý do team nên review test cẩn thận như review production code.

Mocking cần được nhìn với sự thận trọng riêng. Mock có thể isolate một phần logic và làm test chạy nhanh. Quá nhiều mock có thể tạo ra một hệ thống tưởng tượng nhỏ, nơi mọi thứ pass vì test đã dạy code nói chuyện với fake collaborator theo một cách rất cụ thể. Kết quả trông an toàn cho tới khi integration fail. Một test suite khỏe thường trộn nhiều level: unit test tập trung cho rule, integration test cho boundary quan trọng, và một vài end-to-end check cho critical flow.

Câu hỏi thời gian nên tính cả chi phí của việc không làm. Một bug trong permission rule có thể tốn nhiều giờ incident response, giải thích với support, và repair data cẩn thận. Một test bị thiếu quanh billing có thể làm mọi thay đổi sau chậm hơn vì không ai muốn chạm vào code. Trong những trường hợp đó, TDD có thể chậm hơn ở bàn phím nhưng nhanh hơn trong suốt đời sống của feature. Phép tính chỉ đúng khi mình tính cả maintenance, không chỉ initial typing.

Những team tốt nhất tôi từng thấy không xem TDD là identity. Họ dùng nó ở nơi nó tạo leverage. Họ viết test trước cho rule khó, thay đổi rủi ro, và code dự kiến refactor. Họ bỏ qua hoặc trì hoãn nó cho thử nghiệm có thể bỏ đi. Họ giữ test readable. Họ xóa test không còn phục vụ behavior. Họ xem vòng lặp là tool để suy nghĩ, không phải huy hiệu.

Vậy TDD có đáng thời gian không? Có lúc rất đáng. Có lúc chưa phải lúc. Câu hỏi tốt hơn là bạn đang đối diện loại uncertainty nào. Nếu chưa chắc product nên làm gì, hãy nói chuyện, prototype và làm rõ. Nếu chưa chắc code sẽ tiếp tục làm đúng sau khi thay đổi, TDD có thể là một khoản đầu tư bình tĩnh và thực tế.

Lần tới khi review hỏi về test, cuộc trò chuyện hữu ích không phải TDD nói chung có tốt không. Nó là thay đổi này mang rủi ro gì, behavior nào cần được bảo vệ, và loại test nào sẽ làm lần thay đổi tiếp theo dễ hơn. Chính cuộc trò chuyện đó là nơi testing ngừng là ceremony và bắt đầu trở thành một phần trong cách team suy nghĩ.

Bạn thấy bài viết thế nào?