หลังจากที่รู้แล้วว่า โครงสร้างที่ดีของ unit test เป็นอย่างไร
ต่อมาขอแนะนำวิธีการทำให้ unit test ดูดีขึ้นไปอีก
นั่นก็คือ การเป็นอิสระจาก code ที่เราพัฒนาจริงๆ
เพื่อให้การทดสอบมีความคล่องตัว
เป็นอิสระต่อส่วนต่างๆ ที่ต้องเกี่ยวข้อง เช่น Database, Network และระบบไฟล์ เป็นต้น
แล้วมันทำอย่างไรล่ะ ?

Isolation = Separation = การแแยกออก
ดังนั้น Test Isolation คือ การแยกส่วนของทดสอบ ออกจาก code จริงๆ
นั่นคือความเป็นอิสระของการทดสอบ

เริ่มด้วยการทำความเข้าใจกับคำว่า Dependency

คือ สิ่งที่ต้องพึ่งพาอาศัยกัน หรือ ต้องเกี่ยวข้องกัน
จะเห็นได้ว่ามันตรงข้ามกับ Isolation นะ

ดังนั้น สิ่งที่เราต้องรู้ก่อนก็คือ
Dependency มันมีหน้าตาอย่างไร ?
และจะแยกมันออกจากกันได้อย่างไร ?

ตัวอย่างจาก code เรื่อง FizzBuzz

ถ้าผมต้องการให้เก็บการเรียกใช้งาน method say() ของ class FizzBuzz
ไว้ใน ฐานข้อมูล  ระบบไฟล์ หรือ ผ่าน API
ซึ่งสิ่งเหล่านี้มันไม่ใช่ส่วนหนึ่งของ unit test เลย
แต่ระบบต้องใช้งานมันด้วยนะ
แล้วเราจะทำการทดสอบอย่างไรดีล่ะ ?

ปัญหาที่เราพบเจอในตอนนี้คืออะไร ?

ข้อแรก
พบว่าในส่วนของฐานข้อมูล ระบบไฟล์ และ API นั้น
เราไม่สามารถควบคุมได้เลย ดังนั้นเราไม่สามารถตรวจสอบอะไรได้เช่นกัน
ตัวอย่างเช่น ถ้า API Server เกิดล่มขึ้นมา การทดสอบของเราก็จะพังไปด้วย
ทั้งๆ ที่ระบบที่เราทำการทดสอบ หรือ SUT (System Under Test) ทำงานได้กติ

ข้อสอง
ส่วนที่เราต้องเกี่ยวข้องด้วย และ หลีกหนีไม่ได้เลยก็คือ ส่วนของฐานข้อมูล ระบบไฟล์ และ API
มีชื่อเรียกสวยๆ ว่า DOC (Depended-On Component)
มันจะทำให้การทำงานของการทดสอบช้าลงไปอย่างมาก
แต่เป้าหมายของทำ unit test คือ การทดสอบอยู่บ่อย ดังนั้นมันต้องทำงานได้อย่างรวดเร็ว
เพื่อจะได้รับ feedback หรือ รู้ว่า ระบบมันทำงานได้หรือไม่ได้กันแน่
คุณคิดว่า ถ้า unit test ต้องเรียกผ่าน API ทุกๆ ครั้ง มันจะทำงานเร็วไหมนะ ?

ข้อสาม
คุณคิดว่าพฤติกรรมของระบบใน DOC มันจะเหมือนเดิมตามที่คาดหวังไว้หรือเปล่านะ
เชื่อเถอะว่า มันไม่เคยเหมือนเดิมหรอกนะ
ดังนั้น มันทำให้เห็นว่า เมื่อคุณทำการทดสอบผ่าน component แบบตรงๆ แล้ว
เราไม่สามารถควบคุมได้ มันจะทำให้การทดสอบเละ หรือ พัง
มีคำเรียกสวยๆ ว่า Test Fragile

แล้วเราจะแก้ไขปัญหาเหล่านี้อย่างไรดีล่ะ ?

คำตอบง่ายๆ ก็แยกส่วนที่เกี่ยวข้องออกมาสิ
โดยแทนที่ DOC ด้วยสิ่งที่เรียกว่า Test Double
ซึ่งมันคือตัวแทนของ DOC นั่นเอง

วิธีการง่ายๆ คือเราจะสร้าง Layer คั่นระหว่าง SUT และ DOC
ทั้ง input และ output ซึ่ง Layer นั่นเรียกว่า Indirect หรือ Abstraction layer

Screen Shot 2557-11-03 at 11.04.59 AM

เริ่มต้นด้วยการควบคุมฝั่งของ Input หรือข้อมูลเข้า ด้วย Stub

ทำการสร้าง Abstraction layer ด้วยการสร้าง interface ชื่อว่า CounterPersistence

และสร้าง implementation ของ CounterPersistence แบบหลอกๆ ขึ้นมา
เพื่อใช้สำหรับการทดสอบ ชื่อว่า StubCounter ดังนี้

ดังนั้นเราสามารถที่จะแยกไม่ให้ส่วนของการทดสอบ และ SUT และ DOC ผูกกันได้
ด้วยการใช้ Dependency Injection (DI) นั่นคือการ inject CounterPersistence เข้าไปใน SUT ดังนี้
ซึ่งผมเลือกใช้การ inject แบบ Constructor Injection ละกัน

เมื่อทำการแก้ไขแล้ว ทดสอบ run unit test ทั้งหมดด้วยนะครับ
จะต้องผ่านทั้งหมด เพราะว่า เรายังไม่ได้ทำการเปลี่ยนฟฤติกรรมใดๆ ของ code ใน SUT นะ

ต่อไปต้องการเก็บข้อมูลตัวเลขที่จะส่งเข้าไปยัง method say() ของ FizzBuzz
ไว้ใน CounterPersistence ด้วยการเพิ่ม method setNumber() เข้าไป ดังนี้

และแก้ไข class StubCounter ดังนี้

สุดท้ายแก้ไขที่ test case ดังนี้

เพียงเท่านี้คุณก็สามารถจัดการส่ง input เข้าไปยัง CounterPersistence ได้แล้วด้วยการใช้ Stub
แน่นอนว่า test ของคุณมัน run ผ่านอย่างแน่นอน
แต่คุณจะรู้ได้อย่างไรว่า พฤติกรรมการทำงานภายในของ CounterPersistence มันทำงานถูกต้องจริงๆ เช่น
คุณจะรู้ได้อย่างไรว่าค่าของตัวเลขที่ส่งไปยัง CounterPersistence มันถูกต้องไหม ?
หรือค่าล่าสุดที่ทำการเรียกใช้งาน method say() มันคือค่าอะไร

เราสามารถแก้ไขปัญหานี้อย่างไรล่ะ ?

ทำการตรวจสอบ Output หรือ ผลลัพธ์ด้วย Spy

นั่นคือ การส่งสายลับเข้าไปยัง CounterPersistence
เช่นถ้าต้องการตรวจสอบว่า method setNumber() ถูกเรียกใช้หรือไม่
ก็ให้ทำดังนี้

จากนั้นใน unit test ก็เพิ่มการตรวจสอบไปดังนี้

ด้วยวิธีการนี้ ทำให้เราสามารถตรวจสอบสถานะ และ พฤติกรรมของระบบได้แล้ว
ว่าทำงานเป็นไปตามที่เราคาดหวังหรือไม่

แล้ว Mock คืออะไร ?

เราสามารถทำการตรวจสอบพฤติกรรมของการทำงานใน method say() ของ FizzBuzz ได้ด้วยการใช้ Mock
ซึ่ง Mock เป็นแนวคิดที่มีความสำคัญใน Test double
ใช้สำหรับการตรวจสอบการทำงานภายในของ CounterPersistence หรือ implement ของ Abstraction layer
ว่าทำงานหรือมีพฤติกรรมเป็นไปตามที่เราต้องการหรือไม่

สามารถทำได้ดังนี้

โดยใน class MockCouter นั้น จะทำการกำหนดสิ่งที่คาดหวังผ่าน constructor
ดังนั้น ถ้าระบบมีการเรียกใช้ method setNumber() แล้ว
จะต้องทำการตรวจสอบว่าค่าที่ส่งเข้ามาจริงๆ กับ สิ่งที่หวังไว้เท่ากันหรือไม่
และทำการเพิ่มตัวแปร done หรือเป็น flag ตัวหนึ่ง
เพื่อใช้ตรวจสอบว่า method setNumber() นั้นถูกเรียกใช้งานจริงๆ
และในการทดสอบ สามารถเรียกผ่าน method verify() ของ class MockCounter นั่นเอง ดังนี้

สังเกตว่า Mock นั้นใช้สำหรับการตรวจสอบพฤติกรรมการทำงานเท่านั้น ไม่ใช่การตรวจสอบ input/output นะ
มันช่วยทำให้เราหาจุดผิดได้ง่าย ว่าส่วนไหนที่มีพฤติกรรมแปลก หรือ ทำงานผิด

สิ่งที่ Mock ต่างจาก Spy ก็คือ

Spy นั้นจะสนใจที่ output เท่านั้น แต่ไม่ได้สนใจว่า กระบวนการทำงานของระบบเป็นอย่างไร
ดังนั้นถ้าเกิดความผิดพลาดในการ Spy จะไปหาจุดที่ผิดยาก
ส่วน Mock นั้นมันจะช่วยทำให้เราหาจุดที่ผิดพลาดได้ง่ายกว่า Spy
และบอกได้ตรงจุดมากกว่า Spy นั่นเอง
จะได้ไม่ต้องมานั่งเปิด dubug หรือทำให้การ dubug ง่ายขึ้น

โดยสรุป

มาถึงตรงนี้น่าจะพอทำให้เห็นว่า Test Isolation มันเป็นอย่างไร และ ทำอย่างไร
ด้วยตัวอย่างแบบง่ายๆ ครับ

บางคนอาจจะถามว่า เขียน test แบบนี้มันยากนะ มีตัวช่วยหรือ Library อะไรบ้างไหม
ผมก็มักจะตอบว่า การเขียนแบบนี้มันยากตรงไหน
คุณเข้าใจมันหรือยัง

ถ้าคุณบอกว่าเข้าใจแล้ว มันเขียนอะไรไม่รู้เยอะไปหมด อยากลด ทำอย่างไรดี ?
คำตอบคือมี Library ให้ใช้เพียบ เช่น

  • Mockito
  • EasyMock
  • jMock

ลองนำไปใช้ดูครับ แต่ผมแนะนำให้เขียนเองก่อนครับ
เรื่องพื้นฐานสำคัญมากจริงๆ