Bạn biết đau đớn ra sao khi làm việc với mã được kiểm thử kém. Mỗi lần bạn sửa bug, bạn tạo ra năm bug khác. Và khi mọi thứ hoạt động, bạn không bao giờ thực sự biết nó được thiết kế như vậy, hay chỉ hoạt động ngẫu nhiên.
Mặt khác, bạn vừa viết cái giống như 200 test để ship một tính năng nhỏ. Bạn liên tục phải thiết kế lại mã đã hoạt động để đạt 100% coverage test. Bạn không thể rung bỏ cảm giác rằng mã được kiểm thử tốt nhất của bạn đang trở nên kém đọc được hơn. Và tệ nhất, bạn đang bắt đầu burned out trên ứng dụng của mình.
Phải có một trung gian. Vậy bao nhiêu kiểm thử là lượng đúng?
Sẽ tuyệt vời nếu có một con số tròn đẹp bạn có thể dùng như quy tắc: gấp hai lần dòng mã test so với mã app, có lẽ, hoặc 95% coverage test. Nhưng ngay cả nói "95% test coverage" cũng mơ hồ.
Coverage có thể là chỉ báo của mã được kiểm thử tốt, nhưng nó không phải bảo chứng của mã được kiểm thử tốt. Tôi đã có app 100% covered có bug nhiều hơn app 85% coverage.
Vì vậy, lượng kiểm thử đúng không thể về một con số. Thay vào đó, nó về thứ gì đó mờ nhạt hơn, và khó định nghĩa hơn. Nó về việc kiểm thử hiệu quả.
Kiểm thử hiệu quả
Kiểm thử hiệu quả là về việc nhận được lợi ích nhiều nhất cho ít công việc nhất. Nghe tuyệt vời, phải không?
Nhưng có rất nhiều điều vào việc kiểm thử hiệu quả hơn. Vì vậy nó giúp nếu suy nghĩ về ba điều cụ thể: kích thước, cô lập, và tập trung.
Kích thước
Integration test tuyệt vời. Chúng gương lại con đường một người thật đi qua ứng dụng của bạn. Chúng test tất cả mã của bạn, làm việc cùng nhau, theo cách nó được dùng trong thế giới thực.
Nhưng integration test chậm. Chúng có thể dài và lộn xộn. Và nếu bạn muốn kiểm thử kỹ một phần nhỏ của hệ thống, chúng thêm nhiều overhead.
Unit test nhỏ hơn. Chúng chạy nhanh hơn. Chúng dễ suy nghĩ, vì bạn chỉ cần giữ một phần nhỏ của hệ thống trong đầu khi viết chúng.
Nhưng chúng cũng có thể giả. Chỉ vì thứ gì hoạt động trong unit test không có nghĩa là nó cũng sẽ hoạt động trong thế giới thực. (Đặc biệt nếu bạn làm nhiều mocking).
Vậy bạn cân bằng những cái đó như thế nào?
Vì unit test nhanh và dễ viết, nó không tốn nhiều để có nhiều chúng. Vì vậy chúng là nơi tuyệt vời để test thứ như trường hợp biên và logic phức tạp.
Một khi bạn có một đống mảnh được kiểm thử tốt của hệ thống, bạn vẫn phải lấp đầy khoảng trống. Bạn phải test cách những phần đó tương tác, và hành trình đầy đủ ai đó có thể lấy qua ứng dụng của bạn. Nhưng vì hầu hết trường hợp biên và logic được test bởi unit test của bạn, bạn chỉ cần một vài bài kiểm thử phức tạp, chậm hơn integration test đó.
Bạn sẽ nghe ý tưởng này gọi là "Kim tự tháp Test." Nó là vài integration test, ngồi trên đỉnh một cơ sở nhiều unit test. Và nếu bạn muốn học thêm về nó, xem chương ba của cuốn sách của tôi, Practicing Rails.
Cô lập
Vẫn vậy, nếu hệ thống của bạn phức tạp, nó có thể mất cái cảm giác như số lượng test vô hạn để cover mọi tình huống bạn có thể gặp. Điều này có thể là dấu hiệu bạn cần suy nghĩ lại thiết kế ứng dụng. Nó nghĩa là các phần của hệ thống phụ thuộc quá chặt vào nhau.
Giả sử bạn có một đối tượng có thể ở một trong vài trạng thái khác nhau:
case user.type when :admin message = admin_message when :user message = user_message when :author message = author_message else message = anonymous_message end if user.preferred_notification_method = :email send_email(message) elsif user.preferred_notification_method = :text send_text_message(message) else queue_notification(message) end
Nếu bạn muốn test mọi đường đi khả thi ở đây, bạn sẽ có 12 tình huống khác nhau để test:
- User là admin,
preferred_notification_methodlà email - User là admin,
preferred_notification_methodlà text - User là admin,
preferred_notification_methodkhông phải cả hai - User là user,
preferred_notification_methodlà email - User là user,
preferred_notification_methodlà text - User là user,
preferred_notification_methodkhông phải cả hai - User là author,
preferred_notification_methodlà email - User là author,
preferred_notification_methodlà text - User là author,
preferred_notification_methodkhông phải cả hai - User là anonymous,
preferred_notification_methodlà email - User là anonymous,
preferred_notification_methodlà text - User là anonymous,
preferred_notification_methodkhông phải cả hai
Có nhiều case test như vậy vì "gửi tin nhắn dựa trên phương thức thông báo" và "tạo tin nhắn dựa trên loại user" bị gắn chặt. Bạn có thể nén được với ít test hơn, nhưng không rõ ràng – và chỉ đang mời bug.
Nhưng nếu bạn tách chúng ra?
message = get_message_based_on_user_type(user.type) send_notification(message, user.preferred_notification_method)
Bây giờ bạn có thể test từng phần riêng biệt.
Cho phần đầu, bạn có thể test rằng tin nhắn đúng được trả về cho mỗi loại user.
Cho phần hai, bạn có thể test rằng một tin nhắn cho trước được gửi đúng dựa trên giá trị preferred_notification_method.
Và cuối cùng, bạn có thể test rằng phương thức cha sẽ chuyển tin nhắn trả về từ do_stuff_based_on_user_type sang send_email_or_text. Vì vậy bây giờ, bạn có 8 trạng thái để test:
- User là admin
- User là user
- User là author
- User là anonymous
preferred_notification_methodlà emailpreferred_notification_methodlà textpreferred_notification_methodkhông phải cả hai- và một test cho phương thức cha
Ở đây, bạn tiết kiệm bốn test bằng cách tách mã ra để test riêng. Trong ví dụ hai, rõ ràng hơn nhiều bạn có thể vượt qua với ít test hơn. Và bạn có thể tưởng tượng như thế nào khi bạn thêm nhiều trạng thái, tách mã ra trở thành ý tưởng tốt hơn nữa.
Mất thời gian và thực hành trước khi bạn tìm được cân bằng tốt nhất giữa cô lập và readability. Nhưng nếu bạn phá vỡ phụ thuộc ở đúng chỗ, bạn có thể vượt qua với ít test hơn nhiều.
Tập trung
Ứng dụng của bạn nên được kiểm thử tốt. Nhưng đó không có nghĩa là mọi phần ứng dụng đáng được cùng lượng chú ý trên test của nó.
Ngay cả nếu bạn nhắm 100% test coverage, bạn vẫn không test mọi thứ. Bạn có thể sẽ không test mọi dòng text trong view, ví dụ, hoặc rằng bạn đang poll cập nhật mỗi năm giây thay vì mười.
Đó là nơi focus đến. Viết ít test hơn, nhưng hữu ích hơn. Và đưa ra quyết định có ý thức nơi bạn có thể chi tiêu tốt nhất thời gian bạn có.
Focus là thứ khác khó làm đúng. Đây là vài câu hỏi tôi tự hỏi giúp tôi tập trung vào test quan trọng nhất:
Nó liên kết chặt chẽ với phần còn lại của app như thế nào? Nếu nó hỏng, bao nhiêu mảnh khác sẽ sụp theo?
Nó có khả năng thay đổi tự nhiên như thế nào? Nếu test fail, đó sẽ là vì bug, hay vì ai đó cập nhật text trong UI?
Tác động của nó hỏng là gì? Tôi sẽ charge thẻ tín dụng ai đó hai lần, hay chỉ kết thúc với text thiếu?
Phần này được dùng thường xuyên như thế nào? Nó critical cho hành vi app, hay là trang about chôn vùi đâu đó trong footer?
Bạn không nên chỉ test phần quan trọng. Nhưng bạn sẽ có app cảm thấy chất lượng cao hơn nếu bạn chi tiêu thời gian test tốt.
Nếu bạn cố test mọi đường đi đơn lẻ ai đó có thể lấy qua app, bạn sẽ không bao giờ ship. TDD giúp, nhưng nó không giải quyết tất cả vấn đề test của bạn.
Đương nhiên, đó không có nghĩa bạn không nên test chút nào.
Bạn có thể dùng kim tự tháp test để giữ test nhỏ. Bạn có thể cô lập và phá vỡ phụ thuộc để biến m * n test case thành m + n. Và bạn có thể ưu tiên, vậy bạn có thể dành nhiều thời gian test phần quan trọng nhất của app.
Vậy, bạn test bao nhiêu? Bạn có xem xét bất kỳ ý tưởng nào này khi xây dựng app? Và bạn làm sao biết phần nào của app để focus? Để lại bình luận và cho tôi biết tất cả về nó!