ลอง 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)