Spring — Test double and Mockito

Pawut Jingjit
3 min readMay 29, 2022

Mock , InjectMock and Spy

TL;NR

  • Unit Test คือ การ Testing เฉพาะส่วนของ Code ( Individual Units of source code )
  • Test double คือสิ่งที่เรา “แทนที่” Production Object เพื่อการ Testing
  • Mockito คือ Framework สำหรับ Mocking สำหรับ JUnit
  • Mock Annotation ใช้สำหรับ Mock Object หนึ่ง ให้มีพฤติกรรมตามที่เรา Assign ใน “when”
  • InjectMock Annotation ใช้คู่กับ Mock Annotation แทนการ New Object เพื่อลดการผูก Dependencies (เสมือน Autowired Annotation)
  • Spy Annotation เสมือน Patial Mock ในบทความนี้ ใช้สำหรับ Mock Method ใน Object ที่ต้องการนำมา Testing

Unit Test

Unit testing is a software testing method by which individual units of source code

ถ้าเพื่อนๆจะทดสอบว่า API /getProducts สามารถใช้การได้หรือไม่ เพื่อนๆจะทำอย่างใด แน่นอนว่าความคิดแรกต้องเป็นลองใช้ Postman มาส่ง GET Method เพื่อทดสอบ แต่จะมีปัญหาคือ

  • ปกติจะมี API จะมี 4 Layer ถ้า API ทำงานไม่ถูกต้อง เราจะไม่ทราบเลยว่าเกิดความผิดพลาดที่ Layer ใด
  • อิงกับ Dependencies จำนวนมาก

แทนที่เราจะทดสอบทั้งเส้น API เราสามารถทดสอบเฉพาะ Controller Layer , Service Layer , … เพื่อที่จะทราบได้ว่า ส่วนไหนที่เกิดปัญหา

การ Testing เฉพาะส่วนของ Code เช่นนี้ ( Individual Units of source code ) เรียกว่า Unit Test

Test double

Test Double is a generic term for any case where you replace a production object for testing purposes.

Test double คือสิ่งที่เรา “แทนที่” Production Object เพื่อการ Testing

  • เราทราบดีว่า Controller Layer จะเรียก Service Layer เป็น Dependencies นั่นหมายความว่า เราไม่สามารถทดสอบเฉพาะ Controller Layer ได้เลย
  • ไม่ใช่เรื่องดีแน่ๆ ถ้าเรา Testing Repository Layer แล้วเราไปเขียนใน DB เลย คงแปลกๆถ้า Testing 100 รายการ จะมี DB เพิ่มขึ้นมา 100 Row

สมมุติเราจะ Testing Controller Layer เราจะสร้าง Object ที่เสมือนตัวแทนของ Service Layer เราจะเรียก Object ตัวแทนนี้ว่า Test double

Mockito

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors.

Mockito เป็น Mocking Framework (ที่เคลมตัวเองว่า) “รสชาติดี” ทำให้เราสามารถสร้าง test ได้อย่าง “clean & simple”

Problem

  • ต้องการสร้าง Unit Test สำหรับ getProduct Service
Product Entity
Product Repository (แต่ในที่นี้ ขอใช้เป็น Service แทน เนื่องจากมีความยุ่งยากในการต่อกับ DB จริง)
Product Service มี method ที่สนใจเป็น getProducts ที่จะไปเรียก getProducts จาก Product Repository

Without Mocking

สังเกตว่า Code ดังกล่าวสามารถทำงานได้อย่างถูกต้อง แต่จะเกิดอะไรขึ้นเมื่อใช้ Testing นี้

  • เราจะไม่ทราบเลย ว่าถ้าเกิด Test นี้เกิด Error ขึ้นมา Error ที่เกิดขึ้น จะเกิดจาก MainService เอง? เกิดจาก Repository? หรือเกิดจากการติดต่อ DB ไม่ได้
  • สมมุติถ้า Test ดังกล่าวต่อ DB จริงๆ รันครั้งแรก ได้ Product 10 ตัว ต่อมาเพื่อน Dev ของเราก็ใช้ DB นี้เช่นกัน รัน Test นี้อีกครั้ง อาจจะได้ Product มากกว่า 10 ตัวก็ได้
  • ถ้าเรากำลัง Test Create Product อยู่ เมื่อเรา Run Test นี้ไป จะมี Product เพิ่มมาใน DB อย่างนั้นหรือ
  • วันดีคืนดี Internet เราไม่ดี ต่อกับ DB ไม่ได้ Test นี้ที่ควรจะทำงานได้ กลับทำงานไม่ได้

เพื่อนๆจะเห็นว่า ถ้าเราไม่ Mock จะเกิดปัญหาอะไรขึ้นบ้าง มาดูวิธีแก้ปัญหากัน

With Mocking

อย่าลืมเพิ่ม Mock Annotation Line 4–5
  • Line 13 เรากำหนดไว้เลย ว่า ถ้า ProductRepository.getProduct() ถูกเรียก จะ Return ค่าที่เรา Assign ไว้ใน Line 9–11 เสมอ
  • สังเกตว่า จากนี้เราจะไม่ต่อกับ Repository จริงอีกต่อไป ไม่ว่า Repository Layer จะทำงานถูกหรือไม่ RepositoryMock จะทำงานถูกต้อง ตามที่เรา Assign ไว้เสมอ
  • เพื่อนๆจะสังเกตว่า Line 15 เราจำเป็นต้อง Set RepositoryMock ซึ่งจะทำให้ Code ผูกกับ Dependencies ภายนอก ซึ่งไม่ใช่วิธีการเขียนที่ดี (ใน Main Autowired มาดีๆ ใน Test กลับต้องมาผูก Dependencies)

InjectMocks

Line 4–5 ใช้ InjectMock แทนการ New Service สังเกตว่า ใกล้เคียงกับการใช้ Autowired Annotation ใน main มาก
  • InjectMock Annotation เสมือน New Object แล้วผูก Mock Dependencies (Line 7–8) กล่าวง่ายๆ คือ Autowired Annotation ของ Test นั่นเอง
  • สังเกตได้ว่า Code ดังกล่าว ลดการผูกกับ Dependencies เหลือเพียง InjectMock Annotation เท่านั้น (ไม่มี New Object)

Spy

จากที่ผ่านๆมา เราจะสังเกตว่า ไม่ว่าเราจะใช้ Mock หรือ InjectMock เราจะไม่สามารถ Mock Method ใน Object ที่เราต้องการ Test ได้

Spy Annotation เสมือน Patial Mock คือ จะ Mock เพียง Method ที่เราสนใจก็ได้

สังเกตว่า ถ้า someDependency เป็น Method ที่อิงกับ Dependency ข้างนอกเยอะมาก จนเราไม่สามารถ Mock ได้หมด เราจะไม่สามารถ Test SomeFunction ได้เลย
Line 4–5 เราสามารถใช้ Spy Annotation เพื่อ Mock Service มาทดสอบ Testing SomeFunction ได้ โดยการ Mock SomeFunction()

อยากจะเตือนเพื่อนๆเรื่องการใช้ Spy เพราะถ้าอิง Dependencies ข้างนอกเยอะมาก สิ่งที่ควรทำจริงๆคือการ Refactor Function ที่เรียก Dependencies ให้สามารถ Testing ด้วยวิธีปกติได้

ส่งท้าย

บทความนี้จะ Coding น้อยหน่อย แต่เน้นเข้าใจความต่างของ Annotation แต่ละตัวแทน เจ้าของบทความมั่นใจว่า ไม่ใช่เจ้าของบทความคนเดียวที่งงกับ Annotation แต่ละตัวแน่นอน

จริงๆเนื้อหาในบทความนี้ ตอนแรกจะเขียนรวมกับบทความ Restful Spring-Boot + Unit Test แต่เขียนไปแล้วรู้สึกรายละเอียดเริ่มเยอะ เลยขึ้นเป็นบทความแยกแทน

สุดท้ายนี้ก็ขอบคุณเพื่อนๆที่อ่านมาถึงตรงนี้นะครับ ถ้าเขียนตรงไหนไม่รู้เรื่อง ตรงไหนผิดพลาด หรืออยากมาคุยกันเล่นๆก็ตาม สามารถติดต่อได้ทาง Page AstralAria

Reference

--

--