ลอง search ใน Google แล้วพบว่ามีหลายบทความที่เขียนเกี่ยวกับเรื่องนี้เยอะอยู่เหมือนกัน ไล่ตามอ่านหลายบทความอยู่จนพอที่จะทำ minimal working example เกี่ยวกับการ Mock ใน Spring Boot โดยใช้ Mockito (แค่ @Mock กับ @InjectMocks) ได้แล้ว ดีใจ~ 🤩🎉
เผื่อใครอยากลองทำตามก็ไปใช้ Spring Initializr สร้างโปรเจคมาก่อน เลือกเป็น Maven หรือ Gradle ก็ได้นะ ส่วนภาษาก็จริงๆ เลือกอะไรก็ได้ ไม่ว่าจะเป็น Java หรือ Kotlin หรือ Groovy แต่ในบทความจะเป็น Java นะครับ (เหตุผล? ตอนที่เขียนบทความนี้ผมรู้จัก syntax ของ Java อยู่ภาษาเดียวครับ 😂) สร้างเสร็จแล้วก็น่าจะได้ ZIP ไฟล์มา เราก็เอาไป import เข้า IDE ตัวที่ถนัดของเรา ไปเริ่มเขียนโค้ดกันเลย
แบบยังไม่ได้ใช้ Mockito
สมมุติว่าเรามีคลาสแบบง่ายๆ อยู่ 2 คลาส
package team.bars.mockito; public class Bear { public String roar() { return "Hello"; } } public class BearService { public String say() { Bear bear = new Bear(); return bear.roar(); } } ตัว BearService แค่สร้าง bear ขึ้นมาแล้วส่งค่าจาก method ที่ชื่อ roar ออกไป เวลาที่เราเขียนเทสก็จะประมาณนี้
package team.bears.mockito; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class BearServiceTest { @Test public void testItShouldReturnHelloFromBear() { BearService bearService = new BearService(); String actual = bearService.say(); assertEquals("Hello", actual); } } ก็ดูปกติไม่มีอะไรเนอะ แต่ว่าแบบนี้มีจุดที่เราสามารถปรับปรุงให้ดีขึ้นได้คือ
- คลาส
BearServiceมีการเรียกBearที่เป็น dependency ข้างใน เสมือนกับว่าเทสนี้ได้ไปทดสอบตัวBearไปด้วยเลยโดยปริยาย ซึ่งความต้องการจริงๆ แล้วเราอาจจะอยากทดสอบแค่ตัวBearServiceพอ - เรามองไม่เห็นว่า
BearServiceได้ไปเรียกBearจริงๆ หรือเปล่าตามที่เราตั้งใจไว้ - ถ้าเป็นกรณีที่
sayของBearServiceไปเรียก API เวลาที่เรารันเทสแล้ว มันก็จะไปยิง API จริงๆ ซึ่งเราคงไม่อยากให้เป็นแบบนั้น
มาลองใช้ Mockito (@Mock กับ @InjectMocks) กัน
ก่อนอื่นเราจะต้องไปเพิ่ม Mockito ให้เป็น dependency ก่อน ถ้าใครใช้ Gradle ก็ให้ไปเพิ่ม dependency ที่ใช้สำหรับตอน compile ตัวเทส (ไม่เอาไปใช้บน production) ใน dependencies ที่ไฟล์ build.gradle ประมาณนี้
dependencies { ... testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3' } ถ้าใครใช้ Maven ก็ให้เพิ่ม dependency ใน dependencies ที่ไฟล์ pom.xml ตามนี้
<dependencies> ... <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.3.3</version> <scope>test</scope> </dependency> </dependencies> รู้ได้อย่างไรว่าต้องเขียน dependency แบบนี้? ดูจาก Maven Repository จ้า 😆
ต่อไปเราจะไปแก้ที่ BearService ก่อน โดยแทนที่เราจะ instantiate ตัว bear ขึ้นมาเอง เราจะใช้เทคนิค dependency injection เพื่อที่เราจะได้ไม่ต้องมา instantiate ใน BearService เอง ซึ่งใน Spring มี annotation ที่ชื่อ @Autowired มาช่วยให้ชีวีตเราง่ายขึ้น โค้ดของ BearService จะได้ตามนี้
package team.bears.mockito; import org.springframework.beans.factory.annotation.Autowired; public class BearService { @Autowired Bear bear; public String say() { return bear.roar(); } } จากนั้นเราก็จะไปแก้เทส BearServiceTest กัน เราอยากจะ mock ตัว Bear เพื่อที่เราจะได้ทดสอบแค่ส่วนของ BearService เราก็จะแก้โค้ดตามนี้
package team.bears.mockito; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(MockitoExtension.class) public class BearServiceTest { @Mock Bear bear; @InjectMocks BearService bearService; @Test public void testItShouldReturnHelloFromBear() { String actual = bearService.say(); assertEquals("Hello", actual); } } ถ้าเราใช้ JUnit 5 (มีคำว่า Jupiter) เวลาเราจะใช้ Mockito เราก็ใส่ @ExtendWith(MockitoExtension.class) ไว้บนคลาส ถ้าใครใช้ JUnit เวอร์ชั่นต่ำกว่านี้ ก็ให้ลง JUnit 5 ครับ อ่านไปอ่านบทความ ใช้ JUnit 5 + Mockito บน Spring Boot กันต่อได้
ทีนี้ @Mock กับ @InjectMock เนี่ยมันคืออะไรนะ? ผมขอใช้ประโยคง่ายๆ ละกัน
- ตัว
@Mockเป็นการบอกว่ามันคือ object ที่เราจะ mock นะ - ตัว
@InjectMocksจะเป็นบอกว่า object ที่เราแปะหัวเนี่ย จะมีการ inject mock เข้าไปนะ
เสร็จแล้วก็ให้ลองรันเทสดูครับ มันควรจะ fail! 💥 เราจะเห็น error ประมาณนี้
expected: <Hello> but was: <null> ดีใจได้เลยครับ มันแปลว่าเรา mock สำเร็จแล้ว 🎉 ทีนี้เราอยากแค่จะทดสอบนะ ว่า bear ที่เรา mock ไว้มันจะโดยเรียกจริงเปล่า? ต้องเขียนโค้ดอย่างไรนะ? จัดไปตามนี้ครับ เราจะใช้ verify กับ times จาก Mockito
package team.bears.mockito; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) public class BearServiceTest { @Mock Bear bear; @InjectMocks BearService bearService; @Test public void testItShouldReturnHelloFromBear() { bearService.say(); verify(bear, times(1)).roar(); } } แล้วถ้าเราอยากจะ Stub ตัว bear สามารถทำได้ด้วยหรือเปล่า? ทำได้ครับ จัดไปตามนี้ เราจะใช้ when จาก Mockito
package team.bears.mockito; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class BearServiceTest { @Mock Bear bear; @InjectMocks BearService bearService; @Test public void testItShouldReturnHelloFromBear() { when(bear.roar()).thenReturn("Grrrrr!"); String actual = bearService.say(); assertEquals("Grrrrr!", actual); verify(bear, times(1)).roar(); } } โค้ดทั้งหมดที่ใช้ในบทความนี้อยู่ที่ GitHub นะครับ ลองเอาไปเล่นกันดู ตรงไหนปรับให้ดีขึ้นได้ ช่วยเปิด pull request มาให้ด้วยนะ 🤣
หลังจากโพสต์ลง Facebook ไป พี่ปุ๋ยมาให้คำแนะนำเกี่ยวกับ @InjectMocks ว่า
🙏 กราบขอบคุณพี่ปุ๋ยมา ณ ที่นี้ด้วยครับ ถ้าใครสนใจรายละเอียดเพิ่มเติมก็ลองไปอ่าน InjectMocks doc กันดูนะ
ทีนี้ผมขอมาแก้คลาส BearService สักเล็กน้อย สุดท้ายแล้วจะได้ตามนี้
package team.bears.mockito; public class BearService { private Bear bear; public BearService(Bear bear) { this.bear = bear; } public String say() { return this.bear.roar(); } } ที่นี้ก็น่าจะชัดเจนแล้วว่าตัว mock จะถูก inject เข้ามาที่ default constructor ของ BearService 🤓

Top comments (0)