เริ่มโปรเจกต์ใหม่ เวลาโค้ด depend on third-party service อาจเป็น ElasticSearch, Resque, billing provider หรือ arbitrary HTTP API คุณ good developer อยากโค้ดนี้ well tested แตทดสอบโค้ดที่ยิง requests ไป service ที่ควบคุมไม่ได้ ยังไง?
ข้ามทดสอบได้ แตเร็วๆ จะ pile โค้ดมากบน foundation รั่ว Untested code ดึง complex code มา และรู้สึกว่าโค้ด dangerous to refactor เพราะไม่มี test coverage รู้สึกปลอดภัย อยากสร้าง stable foundation แตจบกับ unmaintainable mess
หลีกเลี่ยงสถานการณ์นี้ง่ายกว่าคิด! ด้วย tools ใหม่และ up-front effort นิดหน่อย คุณ decouple tests จาก services ที่โค้ด depend เขียนโค้ดง่าย และมีมั่นใจปรับปรุงโค้ดโดยไม่ introduce bugs แทน procrastinate เพราะไม่รู้เขียนทดสอบต่อ ให้ดู interaction ระหว่างโค้ดและ outside world รู้ทันทีว่าจะ jump เข้าไปกลางยังไง
Mocha: quick-and-dirty approach
Mocha วิธีง่ายที่สุดได้ระหว่างโค้ดและ outside world
ตัวอย่าง มี Cart object trigger credit card charge ตอน checkout อยากแน่ใจว่า cart มี error message ถ้า charge fail
ไม่อยาก tests hit billing system ทุกครั้ง รัน tests แตก็ยาก force service return failure นี่ 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
endMocha ยัง fail tests ถ้า methods ไม่ถูกเรียกแบบที่คาดหวัง:
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!
endMocha 簡單แต handy ต้องระวัง mock แค่ behavior ไม่อยากเกิดง่าย mock มากซ่อน real bugs อย่าจะ overboard: tests เต็ม expects และ stubs อ่านคิดยาก
Test fakes: วิธีโปรดของผม
ถ้า mock/stub methods เดียวกันบน objects เดียวกันตลอด promote mocks เป็น full-fledged objects (เรียกว่า test fakes) แบบนี้:
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
endFakes ดี:
Fake track internal state ของตัวเอง
Fake มี custom assertion messages และ helper functions ทำทดสอบง่าย เช่น
total_chargesmethod ตัวอย่างเป็น full-fledged object ได้ editor และ language support เพิ่ม
editor support autocomplete, inline docs, และสิ่งที่ไม่ได้จาก stubbing methods กับ Mocha validations, exception handling ดีกว่า
用 fake ใน development mode ไม่ต้อง connection real service
เขียน app บนรถบัส ไม่ต้อง forest services รันกินแบตลaptop และ setup fakes คืน data สำหรับ edge cases โดยไม่ setup มาก
Objects นี้用นอกทดสอบได้
นี่ favorite ส่วนของ fakes logging client log ไป 3rd party service และ fake backed by in-memory array dump contents array ใน admin view บนเว็บ verify ว่ากำลัง log สิ่งที่คิดง่ายขึ้น
ทำได้แบบนี้:
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"]เขียน fake uses effort มากกว่า stub methods แตฝึกแล้วไม่เกิน 1-2 ชั่วโมงได้ fake helpful ถ้าสร้างอันมีประโยชน์คนอื่น share it! ผมสร้าง resque-unit นานแล้ว คนเยอะยัง用
inject objects นี้ยังไง?
ต้องทำให้ objects under test คุยกับ fakes โชคดี Ruby abuse ง่าย inject fakes มักไม่ยาก
ควบคุม API ของ object under test เพิ่ม default parameter, attribute, หรือ constructor option ตั้ง fake:
class Card
attr_reader :provider
def initialize(items, options={})
@provider = options.fetch(:provider) { RealProvider.new }
end
endสะอาดตอนคุย real service และมี hook เพิ่ม flexibility ภายหลัง
ไมेकวบคุม object หรือไม่อยากเพิ่ม parameter monkey patch ได้:
# if in test mode
Card.class_eval do
def provider
@provider ||= TestProvider.new
end
end醜ใน test แตสะอาดใน environments ไม่ใช้ fake
เริ่มสร้าง fake ของตัวเองตอนนี้
สร้าง fakes ง่ายขึ้นตอนฝึก ลองตอนนี้:
- หาทดสอบคุย external service Tests ที่ fail ถ้า disconnect internet candidates ดี
- หา object ที่ communicate จริง และ calls ที่โค้ดทำไป object นั้น
- สร้าง duplicate ว่างเกือบของ class นั้น log calls ไป array
- เพิ่ม method คืน list of calls made
- Swap real object ด้วย fake ใหม่ เขียน assertions ต่อ calls ที่โค้ดทำ
ลองดู บอกผมผล!
ด้วยเทคนิคเหล่านี้ ไม่เกินนานคุณจะ tame craziest interactions ระหว่าง app และ outside world Stub 簡單ที่ถูกที่ จะ ship well-tested code ด้วยมั่นใจ