Bạn bắt đầu project mới và đến lúc code phụ thuộc vào service bên thứ ba. Có thể là ElasticSearch, Resque, billing provider, hoặc chỉ HTTP API tùy ý. Bạn là dev tốt, nên muốn code này well tested. Nhưng làm sao test code bắn request đến service hoàn toàn ngoài kiểm soát?
Bạn có thể bỏ test, nhưng bạn sẽ nhanh chóng đắp code phức tạp hơn lên nền tảng lung lay. Code không test tend to attract code phức tạp hơn, và bạn sẽ cảm thấy code quá nguy hiểm để refactor vì không có test coverage cần để an toàn. Bạn muốn xây nền tảng vững cho công việc tương lai, nhưng thay vào đó bạn kết thúc với đống rối unmaintainable.
Tránh tình huống này dễ hơn vẻ ngoài! Với vài công cụ và chút effort trước, bạn có thể decouple test khỏi service code phụ thuộc, viết code đơn giản hơn, và có tự tin cải thiện code đã viết–không gây bug. Thay vì trì hoãn vì không biết cách approach viết test tiếp theo, bạn có thể nhìn interaction giữa code và outside world, và biết chính xác cách nhảy vào giữa nó.
Mocha: cách nhanh và bẩn
Mocha là cách dễ nhất đưa mình giữa code và outside world.
Ví dụ, nói bạn có object Cart trigger credit card charge khi checkout. Bạn muốn đảm bảo cart có error message nếu charge fail.
Bạn có thể không muốn test thực sự hit billing system mỗi lần test chạy. Ngay cả nếu muốn, có thể khó ép service trả về failure. Đây là hình ảnh với Mocha:
def test_error_message_set_on_charge_failure cart = Cart.new(items) cart.stubs(:charge!).returns(false) # mocha in action cart.checkout! assert_equal "The credit card could not be charged", cart.credit_card_error end Mocha cũng có thể fail test nếu method không được gọi như mong đợi:
def test_only_bill_once_per_cart cart = Cart.new(items) cart.expects(:charge!).once # Don't double-bill, no matter how many times we check out cart.checkout! cart.checkout! end Mocha đơn giản, nhưng có thể cực kỳ handy. Bạn phải cẩn thận chỉ mock chính xác hành vi bạn không muốn xảy ra – dễ mock quá nhiều và che bug thật. Bạn cũng không muốn lạm dụng: test đầy expects và stubs khó đọc và suy nghĩ.
Test fakes: cách tôi thích
Nếu bạn mock hoặc stub cùng method trên cùng object luôn, bạn có thể promote mock thành object đầy đủ (đôi khi gọi test fakes), như sau:
def test_billed_full_amount_minus_discount test_payment_provider = TestPaymentProvider.new # A fake payment provider cart = Cart.new(items, discount: 30, provider: test_payment_provider) cart.checkout! assert_equal items.sum(:&price) * 0.7, test_payment_provider.total_charges end Fakes tuyệt vời:
-
Fake của bạn có thể theo dõi internal state
Fake có thể có custom assertion message và helper function làm viết test dễ hơn, như method
total_chargesví dụ trên. -
Là object đầy đủ, bạn có extra editor và language support
Nếu dùng editor support, bạn có autocomplete, inline doc, và điều khác bạn không có bằng stub method riêng lẻ với Mocha. Bạn cũng có validation tốt hơn, exception handling, và bất cứ gì bạn muốn build vào fake.
-
Nếu dùng fake ở development mode, bạn không cần connection đến service thật
Bạn có thể viết app trên xe buýt, không cần rừng service chạy hao pin laptop, và setup fake service trả về data cần để xử lý edge case mà không cần setup nhiều.
-
Đối tượng này có thể dùng ngoài test
Đây có lẽ phần tôi thích nhất của fakes. Bạn có thể có logging client log đến cả service thứ 3 và fake, backed by in-memory array. Bạn có thể dump nội dung array này trong admin view trên site, làm cho việc verify bạn log điều bạn nghĩ dễ dàng hơn nhiều.
Bạn có thể làm như sau:
fake_backend = FakeBackend.new LoggingService.backends = [RealBackend.new, fake_backend] LoggingService.debug("TEST MESSAGE PLEASE IGNORE") fake_backend.messages.first # => [:debug, "TEST MESSAGE PLEASE IGNORE"] Viết fake tốn effort hơn stub method riêng, nhưng với practice không nên quá giờ hoặc hai để có fake hữu ích. Nếu bạn build fake hữu ích cho người khác, hãy share! Tôi build resque-unit lâu lắm, và nhiều người vẫn dùng đến nay.
Làm sao inject đối tượng này?
Bạn phải đưa object under test nói chuyện với fake này. May mắn Ruby dễ abuse nên inject fake thường không khó.
Nếu bạn kiểm soát API của object under test, tốt nhất thêm default parameter, attribute, hoặc constructor option set fake:
class Card attr_reader :provider def initialize(items, options={}) @provider = options.fetch(:provider) { RealProvider.new } end end Điều này sạch khi nói chuyện với service thật và cho hook thêm flexibility sau.
Nếu bạn không kiểm soát object hoặc không muốn thêm parameter, bạn luôn có thể monkey patch:
# if in test mode Card.class_eval do def provider @provider ||= TestProvider.new end end Nó xấu trong test, nhưng sạch trong môi trường không dùng fake.
Bắt đầu xây fake của bạn ngay
Xây fake dễ hơn với practice, nên bạn nên thử ngay:
- Tìm test nói chuyện với external service. Test fail nếu ngắt net là candidate tốt.
- Xác định object nào thực sự communicate, và code gọi gì đến object đó.
- Tạo duplicate hầu như trống của class object đó, và log call bạn làm vào array.
- Thêm method trả về list call đã làm.
- Swap object thật với fake mới, viết assertion chống lại call code bạn làm.
Nếu bạn thử, cho tôi biết kết quả!
Với kỹ thuật này, không còn xa ngày bạn điều khiển được interaction điên rồ giữa app và outside world. Một stub đơn giản ở nơi đúng sẽ cho bạn ship code well-tested với tự tin.