Unit test part I — Mocker

Pun Pakorn
AMPOS Developers
Published in
3 min readMar 15, 2018
เผื่อคนไม่เก็ท https://www.youtube.com/watch?v=crR6pBGMZ90

เวลาเขียน 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

คนละ 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) ฮือๆ>

--

--