loading...

การเริ่มใช้ Mockito (@Mock กับ @InjectMocks) ใน Spring Boot

zkan profile image Kan Ouivirach ・3 min read

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

ก็ดูปกติไม่มีอะไรเนอะ แต่ว่าแบบนี้มีจุดที่เราสามารถปรับปรุงให้ดีขึ้นได้คือ

  1. คลาส BearService มีการเรียก Bear ที่เป็น dependency ข้างใน เสมือนกับว่าเทสนี้ได้ไปทดสอบตัว Bear ไปด้วยเลยโดยปริยาย ซึ่งความต้องการจริงๆ แล้วเราอาจจะอยากทดสอบแค่ตัว BearService พอ
  2. เรามองไม่เห็นว่า BearService ได้ไปเรียก Bear จริงๆ หรือเปล่าตามที่เราตั้งใจไว้
  3. ถ้าเป็นกรณีที่ 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 เนี่ยมันคืออะไรนะ? ผมขอใช้ประโยคง่ายๆ ละกัน

  1. ตัว @Mock เป็นการบอกว่ามันคือ object ที่เราจะ mock นะ
  2. ตัว @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

🙏 กราบขอบคุณพี่ปุ๋ยมา ณ ที่นี้ด้วยครับ ถ้าใครสนใจรายละเอียดเพิ่มเติมก็ลองไปอ่าน 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 🤓

Discussion

pic
Editor guide