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