Viết code cảm thấy dễ dàng hơn viết test cho nó nhiều, và method một dòng kia thực sự cần test à? Nó quá tầm thường! Test bạn thêm chỉ làm double hoặc triple thời gian dev, và lần sau bạn sửa code, bạn cũng phải sửa test. Nó có vẻ như lãng phí, đặc biệt khi bạn chỉ còn chút thời gian trên estimate.
Nhưng sớm muộn, code của bạn chỉ 20% được cover bởi test, và bất kỳ thay đổi nào bạn làm cho code cảm giác như cố thay lớp giữa nhà bài bông không làm đổ cả thứ. Đâu đó, có điều sai sót và dù quyết định bạn làm có vẻ đúng lúc đó, bạn vẫn kết thúc với codebase hoàn toàn unmaintainable.
Làm sao bạn đến đây? Bạn muốn test cung cấp safety net và cho phép refactor tự tin. Chúng nên giúp code tốt hơn! Thay vào đó, bạn quay lại low test coverage trên code bạn không hiểu nữa, và test bạn có khiến việc thay đổi code khó hơn.
Đây không phải thất bại kỹ năng. Nó xảy ra với dev tốt nhất. Nó là thất bại quy trình. Với vài thay đổi cách bạn viết feature mới, bạn có thể bảo vệ code mà không làm test chậm bạn. Test có thể làm code dễ hiểu và linh hoạt hơn. Bạn có thể thay đổi code với tự tin, biết mọi đường đi qua nó được test.
Bạn không nên phải quyết định
Nếu bạn ngồi bàn phím cố quyết định xem có nên test đoạn code không, bạn đã đi sai đường. Bạn nên luôn default "Test it!" Dù nó có vẻ tầm thường không cần test, hãy viết test.
Nếu code tầm thường đến mức viết dễ, nó nên dễ test. Và code phức tạp sẽ không bao giờ tầm thường như ngay sau khi bạn viết nó. Làm sao bạn biết nó vẫn tầm thường sáu tháng sau?
Nhưng bạn không muốn overtest
Test suite khổng lồ có thể là vấn đề riêng. Test chạy 20 phút tốt như không test, vì bạn không chạy chúng thường xuyên. (Bạn nói bạn sẽ, nhưng tôi biết bạn không). Thậm chí tệ hơn, nếu bạn có quá nhiều brittle test, nó làm refactor đau đớn hơn trước, nên bạn không làm. Bạn sẽ kết thúc method dài hơn hầu hết novel.
Điều này mâu thuẫn với điểm trước? Không nhất thiết. Test của bạn nên luôn tập trung vào interface của code, không phải implementation. Ví dụ:
class Cart def initialize(item_params) @line_items = Array(item_params).map {|item| LineItem.new(item[:name], item[:price])} end def total @line_items.sum(&:price) end end Thực sự cảm thấy code ở đây cần test cả class Cart và LineItem. Nhưng class LineItem có được dùng bởi thứ khác không? Nếu nó chỉ là implementation detail của Cart, và không expose ra bên ngoài, nó thực sự cần bao nhiêu test? Không thể chỉ test qua class Cart của bạn?
Class được extract bởi refactor thường không cần test suite riêng. Chúng chỉ là implementation detail. Chỉ khi chúng được dùng độc lập thì cần test thêm.
Với test suite tuyệt vời cho public interface, bạn có flexibility thay đổi implementation không cần viết lại test. Bạn làm được với ít effort hơn nhiều so với viết test suite trung bình cho implementation object.
Phân bổ chi phí test với Test-Driven Development
Ở phần đầu, bạn học nên test mọi thứ. Ở phần hai, bạn học chỉ test public interface. Là Test-Driven Development gộp hai mục tiêu đối lập này lại.
Với Test-Driven Development, test drive thiết kế và implementation của code bằng quy trình:
- Viết failing test giả định code bạn cần đã có.
- Viết implementation đơn giản nhất pass test.
- Refactor loại bỏ duplication (hoặc làm code expressive hơn).
- Chạy test lại (đảm bảo vẫn pass).
- Quay lại bước 1.
Bằng cách làm theo bước này, bạn sẽ test mọi thứ (vì không code nào được viết tanpa failing test), trong khi chỉ test public interface (vì bạn không viết test mới ngay sau refactor).
Nó không bao giờ dễ dàng như vậy. Nhưng vẫn có cách test-drive ngay cả code phức tạp nhất.
TDD có side benefit:
- Bạn sẽ có object model linh hoạt, tested hơn (đây có lẽ là lợi ích chính).
- Hệ thống theo định nghĩa testable, làm test tương lai rẻ hơn.
- Bạn phân bổ chi phí test qua quy trình dev, làm estimate chính xác hơn.
- Nó giữ bạn trong flow, vì bạn không bao giờ phải quyết định làm gì tiếp theo.
Vậy tôi bắt đầu như thế nào?
Bắt đầu là phần khó! Khi bạn vào nhịp viết code test-driven, khó dừng lại.
Lần sau bạn làm feature mới, hãy thử làm theo bước TDD trên. Bạn sẽ có gần 100% code coverage với ít công sức nhất, foundation vững chắc, và tự tin tuyệt đối test suite bảo vệ bạn khi phải sửa code năm sau. Hoặc hôm nay sau, khi requirement đổi lại.
Khi xong, email cho tôi biết kết quả.
Bằng cách tuân theo quy trình testing thẳng thắn, bạn dành ít thời gian hơn cho quyết định đơn giản và truy lùng bug, và nhiều thời gian hơn viết code giải quyết nhu cầu khách hàng và business.