Software Testing Series

Software Testing Series: Test Double Patterns

Tinnapat Chaipanich
KBTG Life
Published in
7 min readNov 29, 2022

--

Image by studio4rt on Freepik

ยินดีต้อนรับสู่บทความซีรีย์ใหม่ที่จะเขียนเกี่ยวกับเรื่องของการทดสอบระบบ โดยจะเน้นไปในการเทสที่ทาง Developer จะต้องเข้าไปเกี่ยวข้องด้วยเป็นหลัก เช่น การทำ Unit Test หรือ Integration Test เป็นต้น โดยในตอนแรกจะเกี่ยวกับสิ่งที่เราเรียกว่า Test Double

สิ่งที่ผมพบในชีวิตการทำงานจริงคือเมื่อมีคนถามว่า Test Double คืออะไร หลายคนก็อาจจะอธิบายว่ามันคือการสร้าง Mock Object เพื่อให้เราสามารถเขียน Unit Test ให้รันจนได้ผลลัพธ์ออกมา แต่พอถามลงไปอีกว่า Mock Object มันมีคุณสมบัติยังไงล่ะ ทีนี้แต่ละคนจะตอบไม่เหมือนกันแล้ว พอไปค้นหาข้อมูลตามอินเทอร์เน็ต ก็จะพบศัพท์อีกหลายคำ เช่น Stub, Fake ที่มีคุณสมบัติและวัตถุประสงค์ต่างกันไปอีก พาลทำให้สับสนว่าจริงๆ แล้วเทคนิคเหล่านี้เหมือนหรือต่างกันอย่างไร ก็เลยจะลองมาเรียบเรียงในบทความนี้ดูครับ

บทความนี้จะไม่ลงลึกพื้นฐานของการเขียน Test โดยใช้ Unit Test Framework ต่างๆ นะครับ ขอสมมุติว่าผู้อ่านมีความรู้เบื้องต้นในการเขียนโปรแกรมโดยใช้ Unit Test Framework อยู่แล้ว ถ้าต้องการศึกษาเพิ่มเติมก็สามารถหาได้ทั่วไปในอินเตอร์เน็ตครับ และถ้ามีโอกาส ผมอาจจะมาเขียนบทความเกี่ยวกับการทำ Unit Test เบื้องต้นอีกครั้ง

xUnit Test Patterns by Gerard Meszaros

เนื้อหาหลักๆ และรูปภาพในบทความนี้ อ้างอิงมาจากหนังสือ xUnit Test Patterns โดยคุณ Gerard Meszaros ซึ่งนิยามของ Test Double แบบต่างๆ ที่เป็นที่ยอมรับเป็นการทั่วไปในการพูดคุยกันบนอินเทอร์เน็ตก็ถูกอ้างอิงจากหนังสือเล่มนี้นี่แหละ สามารถอ่านเนื้อหาฉบับเต็มแบบออนไลน์ที่เว็บไซต์ด้านล่างนี้เลยครับ

และตัวอย่างโค้ดในบทความนี้จะเขียนโดยใช้ Framework JUnit และ Mockito ครับ

มารู้จักคำศัพท์ที่เกี่ยวข้องกันนิดนึง

ถ้าเราอ่านบทความภาษาอังกฤษในเรื่องที่เกี่ยวกับการทดสอบระบบ เราอาจจะพบกับศัพท์เฉพาะหลายคำที่เป็นคำย่อหรือคำที่ใช้เรียกสั้นๆ แล้วเข้าใจโดยไม่ต้องอธิบายให้ยืดยาว ซึ่งในบทความนี้ผมก็จะขอใช้คำเหล่านี้ทับศัพท์ไปเลย เนื้อหาจะได้ไม่เยิ่นเย้อ

  • System Under Test (SUT) คือสิ่งที่ถูกเทสนั่นแหละ ซึ่งอาจจะแตกต่างกันออกไปตามแต่ละระดับของการเทส เช่น ถ้าเรากำลังเขียน Unit Test สิ่งที่ถูกเทสก็จะเป็น Class, Object หรือ Method ที่เราต้องการเทส ถ้าเป็นการเทสในระดับที่สูงกว่า เช่น System Test สิ่งที่ถูกเทสหรือ SUT อาจจะเป็นฟีเจอร์หนึ่งของระบบที่เรากำลังจะเทส หรืออาจจะเป็นระบบทั้งระบบเลยก็ได้
  • Depended-on Component (DOC) คืออะไรก็ตามที่ System Under Test ต้องมีปฏิสัมพันธ์ด้วยและต้องพึ่งพาในการทำงาน ส่วนใหญ่จะอยู่ในลักษณะของการเรียกใช้งาน Function, Method หรือเรียกใช้งานผ่าน API เป็นต้น
  • Indirect Output คือพฤติกรรมของ System Under Test ที่เราไม่สามารถรับรู้ หรือ System Under Test ไม่สามารถแสดงผลออกมาให้เราเห็นได้โดยตรง เช่น สมมุติว่าเราทดสอบโปรแกรม A แล้วโปรแกรม A ต้องมีการไปเรียกโปรแกรม B อีกที ในกรณีนี้เราถือสิ่งที่โปรแกรม A ตอบกลับออกมาว่าเป็น Direct Output เพราะสามารถเห็นได้โดยตรง แต่ Output ที่โปรแกรม A ส่งออกไปเพื่อเป็น Parameter ให้โปรแกรม B เราจะไม่สามารถเห็นได้โดยตรง ถือเป็น Indirect Output ซึ่งในการตรวจสอบ Indirect Output จะต้องใช้เทคนิคบางอย่างเพื่อให้เราสามารถเข้าไปดักหรือดูปฏิสัมพันธ์ระหว่าง Component เหล่านั้นได้
  • Indirect Input ทำนองเดียวกับ Indirect Output เป็น Input ที่เราไม่ได้ส่งเข้าไปหา System Under Test เองโดยตรง แต่เป็น Input ที่ System Under Test ต้องได้รับมาจาก Component อื่นๆ เช่น ผลลัพธ์จากการเรียก Method อื่น จากการเรียก API หรือ Database เป็นต้น

Test Double คืออะไร?

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

เมื่อเราเขียนเทสที่ไม่สามารถใช้ (หรืออาจจะไม่ต้องการใช้) Depended-on Component (DOC) ตัวจริง เราสามารถนำ Test Double มาแทนที่ได้ โดย Test Double ไม่จำเป็นต้องทำงานได้เหมือน Depended-on Component ตัวจริงทุกประการ เพียงแค่ต้องมี API ที่เหมือนกับตัวจริงที่จะทำให้ SUT มองว่ามันเป็นตัวจริง

Test Double

ภาพด้านบนเป็นองค์ประกอบที่เกี่ยวข้องในการเทส อธิบายได้ดังนี้

ซ้าย: โปรแกรมที่ใช้ทำการทดสอบ

หรือโค้ดที่เราเขียนด้วย Unit Test Framework ต่างๆ นั่นเอง แบ่งย่อยออกเป็น 4 ช่อง ตามขั้นตอนมาตรฐานในการเขียนโปรแกรมเทส ดังนี้

  1. Setup เป็นการเตรียมความพร้อมในการรันเทส ซึ่งการสร้าง Test Double ก็จะถูกทำขึ้นมาในขั้นตอนนี้
  2. Exercise เป็นการ Execute หรือส่ง Input เข้าไปหา System Under Test ตาม Test Case ที่กำหนด
  3. Verify เป็นขั้นตอนในการตรวจสอบผลลัพธ์ของ Test Case ว่าผลลัพธ์ที่ได้จริง (Actual) ตรงกับผลลัพธ์ที่คาดหวัง (Expected) หรือไม่
  4. Teardown เป็นขั้นตอนที่ต้องทำหลังการรันเทสจบในแต่ละ Case (ถ้ามี)

ขอเขียนเกี่ยวกับส่วนนี้แค่สั้นๆ ถ้ามีโอกาสจะมาอธิบายแบบละเอียดให้อีกทีนึงครับ

กลาง: System Under Test (SUT)

คือโปรแกรมหรือสิ่งที่เรากำลังจะทดสอบตามที่ได้กล่าวไปในตอนต้น

ขวา: Depended-on Component (DOC)

คือสิ่งที่ System Under Test ของเรามีความเกี่ยวข้องด้วยหรือต้องใช้ตอนทำงาน

จาก SUT เราจะเห็นว่ามีลูกศรวิ่งไปทางขวาไปหา DOC แล้ววิ่งกลับมา แสดงให้เห็นว่าความสัมพันธ์ระหว่าง SUT และ DOC มีทั้งการที่ SUT ต้องส่งข้อมูลหรือมีการเรียกใช้ DOC และ SUT ก็ต้องการผลลัพธ์​หรือ Output จาก DOC ตอบกลับมาด้วย

ระหว่าง SUT กับ DOC ก็คือ Test Double เป็นการแสดงให้เห็นว่าเราใช้ Test Double มาเป็นตัวแทนของ DOC ทำให้เราสามารถทำการทดสอบ SUT โดยที่ไม่จำเป็นต้องมี DOC ตัวจริงเหล่านั้น

Test Double มีเทคนิคอะไรบ้าง?

เวลาพูดถึง Test Double เราอาจคุ้นเคยกับคำว่า Mock ซึ่งกลายเป็นคำที่เรียกกันติดปากไปแล้ว เมื่อจะเขียน Unit Test ก็ต้องมีการทำ Mock นั่นนู่นนี่ แต่ถ้าอ้างอิงตามตำราของคุณ Gerard เค้าได้แบ่ง Test Double ออกเป็น 5 ประเภทด้วยกัน ตามคุณสมบัติที่เราต้องการจาก Test Double ที่แตกต่างกันออกไป ดังนี้

Type of Test Doubles

โดยในแต่ละเทคนิคเราจะเริ่มว่าโจทย์หรือปัญหาคืออะไร และเทคนิคที่ใช้ในการแก้ปัญหาคืออะไร ตามด้วยตัวอย่างโค้ดครับ

1. Dummy

โจทย์:

  • ถ้าในเทสของเราต้องมีการใช้ Parameter ใดๆ ที่ไม่ได้เกี่ยวข้องหรือไม่ได้ใช้งานที่เกี่ยวข้องกับการประมวลผลที่เราต้องการเทสเลย เราจะต้องส่งค่าอะไรเข้าไป

เทคนิคที่ใช้:

  • ส่ง Object ที่ไม่มี Implementation เข้าไปเป็น Parameter แทน

ในกรณีที่ System Under Test ต้องการ Object เข้าไปเป็น Parameter บางอย่างเพื่อให้เมื่อรันแล้วไม่ตาย ไม่ Error หรือทำให้เทสของเรา Compile ผ่าน แต่เราไม่ได้ต้องการทดสอบ Object ตัวนี้หรือไม่ได้มีการเรียกใช้งานเลย ในกรณีนี้ เราอาจจะส่ง Dummy Object เข้าไปเป็น Parameter เพื่อให้โปรแกรมทดสอบสามารถรันได้ โดย Dummy Object นี้ อาจจะไม่ต้องมี Business Logic ใดๆ

เทคนิคนี้มีชื่อเรียกแตกต่างกันไปอีกหลายแบบ เช่น Null Object Pattern หรือ Special Case Pattern

ตัวอย่าง:

สมมุติว่าเรามี Class CalculatorService ซึ่งภายในมี Object Logger สำหรับใช้เขียน Log

แล้วเราต้องการทดสอบ Method add() ของ CalculatorService จะเห็นว่าเราจำเป็นต้องมี Object Logger ด้วย แต่ไม่ได้จำเป็นต่อการทำงานของ CalculatorService และไม่ได้มีผลกระทบกับสิ่งที่เราจะเทส

ดังนั้นเราสามารถส่ง Dummy Object เข้าไปแทนที่ Logger ตัวจริงได้

Fake

โจทย์:

  • เราจะตรวจสอบ Logic การทำงานของโปรแกรมโดยอิสระได้อย่างไร ถ้า Object อื่นที่โปรแกรมนี้ต้องใช้ไม่สามารถใช้งานได้
  • เราจะหลีกเลี่ยงปัญหาว่าการรันเทสใช้เวลานานมากเกินไปได้อย่างไร

เทคนิคที่ใช้:

  • แทนที่ Component ที่ System Under Test ต้องเรียกใช้ด้วย Object ที่ทำงานได้แบบเดียวกัน แต่มีความซับซ้อนน้อยกว่าและทำงานได้รวดเร็วขึ้น

เรามักใช้ Fake ในกรณีที่เราต้องการทดสอบบางอย่างที่ต้องพึ่งพา Component ภายนอก เช่น Database หรือ API ซึ่งตัวอย่างของ Fake คือการนำ In-memory Database มาใช้ในการทดสอบ เพื่อทดแทน Database จริงๆ ที่มีข้อจำกัดในหลายกรณี เช่น Database ตัวจริงสามารถเซ็ตอัพได้ยากหรือทำงานได้ช้า

ตัวอย่าง:

จากตัวอย่าง เรามี Interface ของ UserRepository สำหรับ Query ข้อมูลของ User และอาจจะมี Implementation จริงๆ ของ Interface ที่เป็นการดึงข้อมูล User ขึ้นมาจาก Database

แต่เวลาทดสอบ เราไม่ต้องการต่อ Database จริง เนื่องด้วยมีข้อจำกัดหลายอย่างตามที่ได้ยกมาข้างต้น ดังนั้นเราจะทำการเขียน Fake Object ขึ้นมา โดยเปลี่ยน Implementation เป็นการเก็บข้อมูล User ไว้ใน Memory แทน เพื่อจะได้ไม่ต้องพึ่งพาการใช้งาน Database จริง เป็นต้น

Stub

โจทย์:

  • เราจะตรวจสอบ Logic การทำงานของโปรแกรมโดยอิสระได้อย่างไร ถ้าภายในลอจิคนั้นต้องการ Indirect Input จาก Component อื่นๆ

เทคนิคที่ใช้:

  • ทำการแทนที่ Object จริงด้วย Object สำหรับเทสที่สามารถป้อน Input ให้โปรแกรมที่เราต้องการเทสได้

หลักการของ Stub ก็คือจะเก็บค่าที่ตายตัวไว้อยู่แล้ว โดยไม่มี Business Logic จริงๆ และจะไม่รับรู้ถึง Parameter ที่มันถูกเรียก โดยจะ Return ค่าตามที่ถูกกำหนดไว้เสมอ ไม่ว่า Parameter นั้นจะเป็นอย่างไร เป็นการให้ Input Data กลับมายังโปรแกรมที่เราต้องการทดสอบ เพื่อให้ทำการทดสอบได้ตามต้องการ

รูปด้านบนแสดงให้เห็นว่าหน้าที่ของ Stub คือการตอบ Return Value กลับมาเป็น Input ของ SUT เท่านั้น โดยไม่สนใจว่า SUT จะมีการส่ง Input อะไรไปให้มันหรือไม่ และในขั้นตอนการ Verify ของโปรแกรมทดสอบ เราก็จะทำการ Verify ที่ SUT เท่านั้น

ตัวอย่าง:

เรามี Class PricingService ซึ่งมี Method calculateNetPrice() ทำหน้าที่คำนวณราคาที่รวมภาษีเข้าไปแล้ว โดยอัตราภาษีจะเอามาจาก Class RateProvider อีกทีนึง

สมมุติว่า RateProvider ตัวจริงต้องไปติดต่อกับระบบภายนอก เพื่อคำนวณอัตราภาษีให้เรา แต่เราไม่มีระบบภายนอกตัวนั้นมาทดสอบกับเราจริงๆ กรณีที่ RateProvider ตอบอัตราภาษีกลับมาเป็น 7% เสมอ เราสามารถใช้ Stub เพื่อช่วยในการเขียนโปรแกรมเทสได้ดังนี้

Spy

โจทย์:

  • เราจะสามารถตรวจสอบพฤติกรรมของ System Under Test ที่มีต่อ Component อื่นๆ ได้อย่างไร
  • เราจะตรวจสอบ Logic การทำงานของโปรแกรมโดยอิสระได้อย่างไร ถ้า Logic นั้นมีการส่ง Output ออกไปหรือมีการเรียกใช้ Component อื่นๆ

เทคนิคที่ใช้:

  • ใช้ Test Double ในการตรวจสอบพฤติกรรม หรือการเรียกใช้งาน Method ที่ System Under Test มีการเรียกออกไปหา Component อื่นๆ เพื่อนำมาตรวจสอบยืนยันภายหลัง

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

ตัวอย่าง:

สมมุติว่าเรามี Class House ซึ่งประกอบไปด้วย Class Door และ Windows ดังนี้

โดยใน Class House จะมี Method close() อยู่ ซึ่งเมื่อถูกเรียก จะไปเรียก Method close() ของทั้ง Class Door และ Windows อีกทีนึง ซึ่งจะไปเซ็ตค่าตัวแปร Close ของ Class Door และ Windows ให้เป็น True

ในกรณีนี้เราต้องการเขียนเทส โดยต้องการตรวจสอบว่าเมื่อ Method close() ของ Class House ถูกเรียก สิ่งที่ต้องเกิดขึ้น มีดังต่อไปนี้

  • Method close() บน Class Door และ Windows ต้องถูกเรียก
  • ค่าของตัวแปร Close ของ Class Door และ Windows ต้องมีค่าเป็น True

ถ้าเราใช้ Object ตัวจริง เราจะไม่สามารถตรวจสอบโดยตรงได้ว่า Method close() ของ Class Door และ Windows ถูกเรียกจริงหรือไม่ (แต่ก็สามารถตรวจสอบได้แบบ อ้อมๆ แทน ด้วยการตรวจสอบค่าของตัวแปร Close แต่กรณีที่ภายใน Method มีความซับซ้อนขึ้น ก็อาจจะตรวจสอบได้ยากว่า Method นั้นถูกเรียกจริงหรือไม่)

เพื่อตอบโจทย์ข้างต้น เราสามารถเขียนเทส โดยใช้หลักการของ Spy ด้วย Mockito Framework ดังนี้

จากโปรแกรมจะเห็นว่าเรายังสามารถตรวจสอบค่าของตัวแปร Close ของ Door และ Windows ที่เปลี่ยนไปได้อยู่ เนื่องจากในโค้ดเรามี Object Door และ Windows ตัวจริงที่ทำงานได้จริง แต่เราใช้ Spy เหมือนเป็นสายลับเข้าไปดัก หรือแอบดู Interaction ว่า Object ตัวจริงนั้นถูกเรียกใช้อย่างไร โดย Object ตัวจริงก็ยังทำงานของมันตามปกติ

Mock

โจทย์:

  • เราจะสามารถตรวจสอบพฤติกรรมของ Indirect Output ของ System Under Test ที่มีต่อ Component อื่นๆ ได้อย่างไร
  • เราจะตรวจสอบ Logic การทำงานของโปรแกรมโดยอิสระได้อย่างไร ถ้าภายใน Logic นั้นต้องการ Input กลับมาจาก Component อื่นๆ ด้วย

เทคนิคที่ใช้:

  • แทนที่ Component ที่ System Under Test ต้องเรียกใช้ด้วย Object จำลองสำหรับทดสอบที่สามารถตอบ Response ที่ต้องการในกรณีที่มี Input หนึ่งๆ ส่งเข้าไป และตรวจสอบได้ว่ามีการเรียกใช้งานจาก System Under Test ตามความคาดหวังหรือไม่

Mock เป็น Object ที่เราต้องกำหนดพฤติกรรมของมันไว้ก่อน โดย Mock Object เวลาที่ถูกเรียกจะรับรู้และบันทึกไว้ด้วยว่าถูกเรียกอย่างไร ด้วย Parameter อะไรบ้าง ซึ่งจะต่างจาก Stub ในมุมที่ว่า Stub จะไม่รับรู้ว่ามันถูกเรียกอย่างไร ด้วย Parameter อะไรบ้าง แต่ Mock จะรับรู้และสามารถสร้าง Output ซึ่งแตกต่างกันออกไปตามแต่ Parameter ที่ได้รับมาได้อีกด้วย

จากภาพ Diagram นี้ จะเห็นความแตกต่างกับภาพของ Stub โดยของ Stub โปรแกรมเทสจะ Verify เฉพาะผลลัพธ์ที่ได้จากการทำงานของ SUT เท่านั้น แต่ในกรณีของ Mock จะมีการ Verify ที่ Mock ด้วยว่า Interaction ที่มีระหว่างมันกับ SUT เป็นตามที่คาดหวังหรือไม่

ตัวอย่าง:

สมมุติว่าเรามี Class User และ Interface UserRepository ที่ทำหน้าที่ดึงข้อมูล User ดังนี้

และเรามี Class UserService ซึ่งมี Method ชื่อว่า getAllUsers ทำหน้าที่ Return ข้อมูล User ทั้งหมดที่มี โดยมีการทำงานดังนี้

  • เรียก Method findAllUserIds บน Object UserRepository โดย Method นี้จะ Return ค่า userId ทั้งหมดที่มีออกมาเป็น List ของ String
  • วนลูป เอา UserId แต่ละตัวใน List ที่ Return จากข้อก่อนหน้านี้ ไปเรียก Method findById ของ UserRepository เพื่อเอา Object User ที่มีข้อมูลอื่นๆ ออกมาด้วย
  • เอา objectUser ที่ได้ไปใส่ใน List และ Return ออกไปหาคนเรียก

ทีนี้เราต้องการทดสอบ Class UserService ที่เราเขียนขึ้นมา ลองช่วยกันคิดครับว่ามี Test Case ไหนบ้างที่เราต้องทดสอบ

หากเราต้องการ Test Case ที่สมมุติว่ามี Data ดังนี้

  • มี User 2 คนในระบบ
  • คนแรก userId = user01, userName = scott
  • คนที่สอง userId = user02, userName = tiger

ดังนั้นตอนจำลอง Response ของ UserRepository เพื่อทดสอบ UserService ของเรา เราก็ต้องกำหนดให้ตัว Test Double ตัวนี้ของเรามีคุณสมบัติดังต่อไปนี้

  • เมื่อ Method findAllUserIds() ถูกเรียก ให้ Return Array ของ String 2 ตัว คือ “user01”, “user02”
  • เมื่อ Method findById() ถูกเรียกโดยมี Parameter เป็น “user01” ให้ Return Object User ที่มีค่าของ userId เป็น “user01” และ userName เป็น “scott”
  • เมื่อ Method findById() ถูกเรียกโดยมี Parameter เป็น “user01” ให้ Return Object User ที่มีค่าของ userId เป็น “user02” และ userName เป็น “tiger”

เพื่อให้ UserService ของเราได้ Input เหล่านี้ไปทำงานตาม Test Case ต่อไป

นอกจากนี้การที่เราจะตรวจสอบว่า UserService ของเรามีปฎิสัมพันธ์กับ UserRepository ได้ถูกต้อง พฤติกรรมที่ต้องเกิดขึ้นคือ…

  • UserService ต้องเรียก Method findAllUserIds() ของ UserRepository
  • UserService ต้องเรียก Method findById() ของ UserRepository โดยส่ง Parameter เข้าไปเป็น “user01”
  • UserService ต้องเรียก Method findById() ของ UserRepository โดยส่ง Parameter เข้าไปเป็น “user02”
  • 3 ข้อก่อนหน้าต้องเกิดขึ้นเรียงลำดับแบบนี้เท่านั้น และหลังจากนั้นต้องไม่มี Method ใดๆ ของ UserRepository ที่ถูกเรียกอีก

และผลลัพธ์ที่ต้องได้จากการรัน Method getAllUser() บน UserService คือ…

  • ต้องได้ Array ของ Object User ที่มีขนาด 2
  • Object แรกใน Array ต้องเป็น Object User ที่มี userId เป็น “user01” และมี userName เป็น “scott”
  • Object แรกใน Array ต้องเป็น Object User ที่มี userId เป็น “user01” และมี userName เป็น “tiger”

ในกรณีนี้ Test Double ของเราถือว่าใช้เทคนิค Mock เนื่องจากในการตอบข้อมูลกลับออกมา Test Double ตัวนี้ต้องสนใจ Input ที่ถูกส่งเข้าไปหาด้วย (ต่างจาก Stub ที่ไม่สนใจ Input) และยังมีการ Verify ปฎิสัมพันธ์ระหว่าง SUT ซึ่งก็คือ UserService กับตัวมันว่าต้องมีปฎิสัมพันธ์ระหว่างกันอย่างไร

ตัวอย่างนี้สามารถเขียนเป็นโปรแกรมเทสได้ดังนี้

สรุปทิ้งท้าย

เทคนิคในการทำ Test Double ตามตำราของคุณ​ Gerard แบ่งเป็น 5 แบบ ประกอบไปด้วย…

  • Dummy เป็น Object ที่ถูกส่งเข้าไปในโปรแกรมแต่ไม่ถูกใช้งานจริง ใช้ในกรณีที่โปรแกรมต้องการ Parameter เป็น Object เอาอะไรก็ได้ใส่เข้าไปให้ครบๆ ให้ทำงานได้เฉยๆ
  • Fake เป็น Object ที่ทำงานได้เสมือน Object จริง แต่เป็นการ Implement แบบง่ายๆ และทำงานได้รวดเร็ว เช่น การนำ In-memory Database มาใช้แทน Database จริงในขั้นตอนการทำเทส
  • Stubs เป็น Object ตัวปลอมที่เรากำหนดไว้แล้วว่าเมื่อถูกเรียกด้วย Input จะให้ตอบ Output อย่างไร โดยตอบได้เฉพาะ Input ที่เรากำหนดไว้แล้วเท่านั้น
  • Spy เป็น Object ตัวปลอมที่สามารถบันทึก Interaction ระหว่าง System Under Test กับตัวมัน เช่น ถูกเรียกกี่ครั้ง
  • Mock เป็น Object ตัวปลอมที่เราสั่งให้มัน Response ในสิ่งที่เราต้องการ ขึ้นอยู่กับ Input ที่เข้าไปหามัน และเราสามารถตรวจสอบที่ตัวมันได้ว่า System Under Test มีปฏิสัมพันธ์กับมันอย่างไรบ้าง ตรงกับที่เราคาดหวังหรือไม่

ซึ่งเทคนิคทั้ง 5 ประเภทนี้อาจมีชื่อเรียกที่แตกต่างจากชื่อที่ใช้ใน Unit Test หรือ Mock Framework ต่างๆ ยกตัวอย่าง เช่น

  • Mock กับ Spy ใน Mockito เป็นคนละความหมายกับในตำราเรื่อง Test Double ในเชิงที่ว่าทั้ง Mock และ Stub ใน Mockito นั้นสามารถใช้เป็นได้ทั้ง Stub, Spy, Mock ในความหมายของ Test Double อยู่แล้ว ขึ้นอยู่กับว่าเรานำคุณสมบัติไหนของมันมาใช้ แต่ความแตกต่างของ Mock กับ Spy ใน Mockito คือ Mock เป็นการสร้าง Object จำลองขึ้นมาใหม่ทั้งตัว โดยที่ไม่ต้องมี Implementation ของ Object ตัวจริง ในขณะที่ Spy คือการทำ Partial Mocking เป็นการไปครอบ Object ตัวจริงอีกที โดยถ้าเราเรียก Method ที่ไม่ได้ถูก Mock ก็จะถูกเรียกไปยัง Object ตัวจริง ดังนั้นการใช้ Spy จึงจำเป็นต้องมี Object ตัวจริงอยู่ด้วย
  • เทคนิคของ Mock กับ Stub ใน Mockito จะใช้คำสั่ง mock() ในการสร้างเหมือนกัน และสามารถใช้งานเป็นแบบ Mock หรือ Stub ก็ได้ แล้วแต่เราจะเขียนโค้ดครับ

ในส่วนของ Source Code ตัวอย่างที่ใช้ในบทความ สามารถดูได้ที่นี่ครับ

หวังว่าบทความจะมีประโยชน์กับผู้อ่านไม่มากก็น้อย สำหรับวันนี้ผู้เขียนขอลาไปก่อน แล้วพบกันใหม่ตอนหน้า สวัสดีครับ :)

สำหรับชาวเทคคนไหนที่สนใจเรื่องราวดีๆแบบนี้ หรืออยากเรียนรู้เกี่ยวกับ Product ใหม่ๆ ของ KBTG สามารถติดตามรายละเอียดกันได้ที่เว็บไซต์ www.kbtg.tech

--

--

Tinnapat Chaipanich
KBTG Life

DEVelopment eXcellence engineer — DEVX@KBTG / Console Gamer