Unit test part I — Mocker
เวลาเขียน test สิ่งหนึ่งที่หลีกเลี่ยงไม่ได้คือการสร้าง Test Doubles
Test Doubles คือ?
คำว่า Double ในที่นี้มีความหมายเดียวกับ Stunt Double หรือตัวแสดงแทนในการถ่ายทำภาพยนตร์ ส่วนในการเขียน test ก็คือการสร้างของที่ไม่ได้ใช้ในโค้ดจริงๆ ขึ้นมาเพื่อให้การทำเทสง่ายขึ้น ที่เหล่า dev ชอบพูดติดปากกันว่า ทำ mock บ้าง ทำ stub บ้าง
เอาจริงๆ เราก็เป็นคนที่เคยสับสนเหมือนกันว่าศัพท์แต่ละอย่างมันต่างกันยังไง เด๋วก็ mock เด๋วก็ stub เด๋วก็ dummy (คิดว่าบางทีคนพูดก็ไม่รู้ว่ามันต่างกัน)
วันนี้เราจะมาว่ากันว่า ความจริงแล้วมันต่างกันยังไงนะ ละแต่ละอย่างควรจะใช้จังหวะไหน
System ที่เราจะ test กัน
class System ของเราจะมี 2 properties คือ authorizer ซึ่งจะใช้ในการยืนยันตัวตน และ authorizedUser ซึ่งจะเก็บ User ที่ผ่านการยืนยันตัวตนเรียบร้อยแล้วไว้ เพื่อจะใช้ในการทำงานต่อๆ ไป
จะเห็นได้ว่าการทำงานของ System จะถูกผูกอยู่กับ Authorizer และโค้ดของ Authorizer ต้องต่อ API กับ backend เพื่อยืนยันตัวตนของผู้ใช้ แบบนี้จะทำให้เราเทส class System ได้ยาก
ถ้าเราใช้ Authorizer จริงๆ ต่อ API จริงๆ แบบนั้นก็จะเรียกว่าเป็น Integration Test แล้ว ไม่ใช่ Unit Test
ถ้างั้นเราควรทำไงล่ะ
เราก็จะเปลี่ยนให้ Authorizer เป็น Protocol (หรือ Interface ในภาษาอื่น) และสร้าง Test Double ของ Authorizer ให้ System ตอนที่จะเทส
มาดูกันดีกว่า ว่าสร้าง Test Double อะไรได้บ้าง และควรใช้จังหวะไหนบ้าง
1. Dummy
สมมติว่าสิ่งที่เราจะเทสคือ ตอนเริ่ม app จะต้องยังไม่มีผู้ใช้ที่ผ่านการยืนยันตัวตน นั่นคือ
ตอนที่ init System ขึ้นมา authorizedUser ต้องเป็น nil
โค้ดที่ใช้ในการเทสเคสนี้จะเป็นประมาณนี้
เราต้องสร้าง dummy ขึ้นมาก็เพราะว่า ตอน init System จำเป็นต้องใส่ Authorizer เข้าไปเป็น parameter แต่ถ้าดูดีๆ จะเห็นว่าเราไม่ได้ใช้ dummy ในการเทสเคสนี้เลย
ใช่แล้ว! นั่นแหละคือหน้าที่ของ dummy
Dummy คือของที่เราจำเป็นต้องใช้ในการทำอะไรสักอย่าง แต่เราไม่สนใจเลยว่ามันจะถูกใช้อย่างไร
You pass it into something when you don’t care how it’s used. — Uncle Bob
2. Stub
สมมติว่า System ของเรามี method นึงที่ใช้ในการเชคว่า authorizedUser ที่เรามี ยัง authorized อยู่จริงๆ รึเปล่า
โค้ดก็จะเป็นประมาณนี้
การเทส method recheckAuthorizedUser อย่างหนึ่งที่ควรมีก็คือ ถ้าผู้ใช้ที่เราเก็บอยู่ ไม่ผ่านการยืนยันตัวตนแล้ว ก็ต้องเปลี่ยนหน้าไปหน้า login นะ นั่นคือ
ถ้ามี authorizedUser และ authorizer.authorize return false ต้องไปหน้า login
คราวนี้เราจะใช้ dummy ไม่ได้แล้วนะ เพราะเราอยากได้ค่า false ตอนเรียก authorize dummy มันรีเทินอะไรซี้ซั้วก็ได้ เพราะเราไม่แคร์อยู่แล้ว
ในเวลาแบบนี้เราต้องใช้ Stub!
Stub คือ Test Double ที่สร้างขึ้นมาเพื่อทำหน้าที่เดียว เช่น ในกรณีนี้คือให้ authorize return false เท่านั้น
Note: อย่าเข้าใจผิดว่า เคสแรกให้ return true เลยเป็น dummy ส่วนเคสสอง return false เลยเป็น stub นะ เคสแรกเราไม่แคร์ผลลัพธ์จะ true หรือ false มันก็ยังเป็น dummy อยู่ดี ส่วนเคสสองพอดีเราอยากได้ผลลัพธ์เป็น false ถ้าอยากได้ผลลัพธ์เป็น true ก็สามารถสร้าง AuthorizerAlwaysTrueStub ได้
การจะเป็น Stub หรือ Dummy ไม่ได้อยู่ที่ว่า return อะไร แต่อยู่ที่ behavior
3. Spy
สมมติว่าเรามี method สักอย่างใน System ที่ต้องเรียก authorizer.authorize เทสเคสนึงที่เราควรเขียนก็คือ method นั้นเนี่ย มันไปเรียก authorizer.authorize จริงๆ นะ
จังหวะนี้เราจะใช้ Spy เพื่อจะเชคว่า authorize ถูกเรียกจริงรึเปล่า แบบนี้
Spy คือ Test Double ที่สร้างขึ้นมาเพื่อตรวจสอบการทำงานบางอย่างของ Caller เช่น กรณีนี้คือตรวจสอบว่า authorize ถูกเรียกและ pass parameter มาถูกต้อง
การใช้ Spy มีข้อควรระวังคือ ควรจะ spy ให้น้อยที่สุดเท่าที่จะเทสได้ (ในกรณีนี้คือ spy parameter 2 ตัว) เพราะว่ายิ่ง spy มากก็จะยิ่งทำให้เกิด Coupling ระหว่าง Spy กับของที่จะเทสมาก และจะนำไปสู่ fragile test คือเทสที่พังง่าย เพียงแค่เราแก้โค้ดเล็กน้อย
4. Mock
คราวนี้เราจะมาพูดถึง Mock จริงๆ ตามนิยามละ ไม่ใช่คำพูดติดปากที่ใครต่อใครใช้กันไปทั่ว
โค้ดที่เราจะเทสด้วย Mock จะเหมือนกับ Spy เมื่อกี้นี้ คือเราจะเทสว่า doSomething เรียก authorize และส่ง parameter ถูกรึเปล่า
ถ้าเทสด้วย Mock จะเป็นแบบนี้
จะเห็นว่าการเทสด้วย Mock จะไม่สนใจค่าที่ Mock return กลับมาเลย แต่จะสนใจ behavior ของ System มากกว่า ว่าทำงานถูกอย่างที่ต้องการรึเปล่า และจะเชคใน method verify ซึ่ง return แค่ Bool เพื่อบอกว่า behavior ถูกมั้ย
Note: verify ไม่จำเป็นต้องรับ parameter ก็ได้ เช่น ถ้าเราอยากจะเทสแค่ doSomething เรียก authorize จริงรึเปล่า ก็จะเขียนแค่นี้ก็ได้
Mock จะ coupling มากกว่า Spy มาก เพราะจะเทสใน Mock เลย ส่วน Spy จะแค่ return ค่าหรือมี property ให้ Test case มาตรวจสอบเอง
5. Fake
ตัวสุดท้ายที่เราจะพูดถึงกันคือ Fake
อันนี้คือตัวอย่างของ Fake ที่จะ return true เมื่อ user เป็น admin อาจจะใช้ในการเทสว่า ค่าเริ่มต้นของระบบต้องมี admin user เสมอ
Fake แตกต่างกับ Stub ตรงที่ Fake จะรู้จัก business logic ด้วย!! เราอาจจะพูดได้ว่า Dummy < Stub < Spy < Mock แต่ Fake คืออยู่อีกโลกนึงเลย ทำให้ตัว Fake จะซับซ้อนกว่าอีกสี่ตัวที่เหลือมากๆ มากจนอาจจะต้องมีเทสเคสเพื่อเทส Fake อีกที
Conclusion
จาก Dummy จนถึง Fake คือเรียงตามลำดับความซับซ้อนของ Test Double แล้ว ในการเขียนจริงๆ เราควรเลือกใช้ตัวที่ซับซ้อนน้อยที่สุดเท่าที่เป็นไปได้ที่ทำให้เราเขียนเทสที่ต้องการได้
จากประสบการณ์จริง ใน project ปัจจุบัน ส่วนใหญ่จะใช้แค่ Stub กับ Spy เท่านั้น
จบแล้ว
<เขียนสามชั่วโมงได้ (3 min read) ฮือๆ>