SpringBoot - 單元測試工具 Mockito

古古

2020/02/20


Mockito就是一種 Java mock 框架,他主要是用來做 mock 測試的,他可以模擬任何 Spring 管理的 bean、模擬方法的返回值、模擬拋出異常…等,在了解 Mockito 的具體用法之前,得先了解什麼是 mock 測試

1. 什麼是 mock 測試? #

mock 測試就是在測試過程中,創建一個假的對象,避免你為了測試一個方法,卻要自行構建整個 bean 的 dependency chain

像是以下這張圖,類 A 需要調用類 B 和類 C,而類 B 和類 C 又需要調用其他類如 D、E、F 等,假設類 D 是一個外部服務,那就會很難測,因為你的返回結果會直接的受外部服務影響,導致你的單元測試可能今天會過、但明天就過不了了

而當我們引入 mock 測試時,就可以創建一個假的對象,替換掉真實的 bean B 和 C,這樣在調用B、C的方法時,實際上就會去調用這個假的 mock 對象的方法,而我們就可以自己設定這個 mock 對象的參數和期望結果,讓我們可以專注在測試當前的類 A,而不會受到其他的外部服務影響,這樣測試效率就能提高很多

2. Mockito 簡介 #

說完了 mock 測試的概念,接下來我們進入到今天的主題,Mockito

Mockito 是一種 Java mock 框架,他主要就是用來做 mock 測試的,他可以模擬任何 Spring 管理的 bean、模擬方法的返回值、模擬拋出異常…等,他同時也會記錄調用這些模擬方法的參數、調用順序,從而可以校驗出這個 mock 對象是否有被正確的順序調用,以及按照期望的參數被調用

像是 Mockito 可以在單元測試中模擬一個 service 返回的數據,而不會真正去調用該 service,這就是上面提到的 mock 測試精神,也就是通過模擬一個假的 service 對象,來快速的測試當前我想要測試的類

目前在 Java 中主流的 mock 測試工具有 Mockito、JMock、EasyMock..等,而 SpringBoot 目前內建的是 Mockito 框架

題外話說一下,Mockito 是命名自一種調酒莫吉托(Mojito),外國人也愛玩諧音梗。。。

3. 在 SpringBoot 單元測試中使用 Mockito #

首先在 pom.xml 下新增 spring-boot-starter-test 依賴,該依賴內就有包含了 JUnit、Mockito

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

先寫好一個 UserService,他裡面有兩個方法 getUserById()insertUser(),而他們會分別去再去調用 UserDao 這個 bean的 getUserById()insertUser() 方法

@Component
public class UserService {

    @Autowired
    private UserDao userDao;

    public User getUserById(Integer id) {
        return userDao.getUserById(id);
    }

    public Integer insertUser(User user) {
        return userDao.insertUser(user);
    }
}

User model 的定義如下

public class User {
    private Integer id;
    private String name;
    //省略 getter/setter
}

如果這時候我們先不使用 Mockito 模擬一個假的 userDao bean,而是真的去 call 一個正常的 Spring bean 的 userDao 的話,測試類寫法如下。其實就是很普通的注入 userService bean,然後去調用他的方法,而他會再去 call userDao 取得 DB 的 data,然後我們再對返回結果做 assert 檢查

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    //先普通的注入一個userService bean
    @Autowired
    private UserService userService;

    @Test
    public void getUserById() throws Exception {
        //普通的使用userService,他裡面會再去call userDao取得DB的data
        User user = userService.getUserById(1);

        //檢查結果
        Assert.assertNotNull(user);
        Assert.assertEquals(user.getId(), new Integer(1));
        Assert.assertEquals(user.getName(), "John");
    }
}

但是如果 userDao 還沒寫好,又想先測 userService 的話,就需要使用 Mockito 去模擬一個假的 userDao 出來

使用方法是在 userDao 上加上一個 @MockBean 注解,當 userDao 被加上這個注解之後,表示 Mockito 會幫我們創建一個假的 mock 對象,替換掉 Spring 中已存在的那個真實的 userDao bean,也就是說,注入進 userService 的 userDao bean,已經被我們替換成假的 mock 對象了,所以當我們再次調用 userService 的方法時,會去調用的實際上是 mock userDao bean 的方法,而不是真實的 userDao bean

當我們創建了一個假的 userDao 後,我們需要為這個 mock userDao 自定義方法的返回值,這裡有一個公式用法,下面這段 code 的意思為,當調用了某個 mock 對象的方法時,就回傳我們想要的自定義結果

Mockito.when( 對象.方法名() ).thenReturn( 自定義結果 )

使用 Mockito 模擬 bean 的單元測試具體實例如下

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @MockBean
    private UserDao userDao;

    @Test
    public void getUserById() throws Exception {
        // 定義當調用mock userDao的getUserById()方法,並且參數為3時,就返回id為200、name為I'm mock3的user對象
        Mockito.when(userDao.getUserById(3)).thenReturn(new User(200, "I'm mock 3"));

        // 返回的會是名字為I'm mock 3的user對象
        User user = userService.getUserById(3);

        Assert.assertNotNull(user);
        Assert.assertEquals(user.getId(), new Integer(200));
        Assert.assertEquals(user.getName(), "I'm mock 3");
    }
}

Mockito 除了最基本的 Mockito.when( 對象.方法名() ).thenReturn( 自定義結果 ),還提供了其他用法讓我們使用

thenReturn 系列方法 #

當使用任何整數值調用 userService 的 getUserById() 方法時,就回傳一個名字為 I’m mock3 的 user 對象

Mockito.when(userService.getUserById(Mockito.anyInt())).thenReturn(new User(3, "I'm mock"));
User user1 = userService.getUserById(3); // 回傳的user的名字為I'm mock
User user2 = userService.getUserById(200); // 回傳的user的名字也為I'm mock

限制只有當參數的數字是 3 時,才會回傳名字為 I’m mock 3 的 user 對象

Mockito.when(userService.getUserById(3)).thenReturn(new User(3, "I'm mock"));
User user1 = userService.getUserById(3); // 回傳的user的名字為I'm mock
User user2 = userService.getUserById(200); // 回傳的user為null

當調用 userService 的 insertUser() 方法時,不管傳進來的 user 是什麼,都回傳 100

Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100);
Integer i = userService.insertUser(new User()); //會返回100

thenThrow 系列方法 #

當調用 userService 的 getUserById() 時的參數是 9 時,拋出一個 RuntimeException

Mockito.when(userService.getUserById(9)).thenThrow(new RuntimeException("mock throw exception"));
User user = userService.getUserById(9); //會拋出一個RuntimeException

如果方法沒有返回值的話(即是方法定義為public void myMethod() {...}),要改用 doThrow() 拋出 Exception

Mockito.doThrow(new RuntimeException("mock throw exception")).when(userService).print();
userService.print(); //會拋出一個RuntimeException

verify 系列方法 #

檢查調用 userService 的 getUserById()、且參數為3的次數是否為1次

Mockito.verify(userService, Mockito.times(1)).getUserById(Mockito.eq(3));

驗證調用順序,驗證 userService 是否先調用 getUserById() 兩次,並且第一次的參數是 3、第二次的參數是 5,然後才調用insertUser() 方法

InOrder inOrder = Mockito.inOrder(userService);
inOrder.verify(userService).getUserById(3);
inOrder.verify(userService).getUserById(5);
inOrder.verify(userService).insertUser(Mockito.any(User.class));

4. Mockito 的限制 #

上述就是 Mockito 的 mock 對象使用方法,不過當使用 Mockito 在 mock 對象時,有一些限制需要遵守

  • 不能 mock 靜態方法
  • 不能 mock private 方法
  • 不能 mock final class

因此在寫 code 時,需要做良好的功能拆分,才能夠使用 Mockito 的 mock 技術,幫助我們降低測試時 bean 的 dependency

5. 總結 #

Mockito 是一個非常強大的框架,可以在執行單元測試時幫助我們模擬一個 bean,提高單元測試的穩定性,並且大家可以嘗試在寫 code 時,從 mock 測試的角度來寫,更能夠寫出功能切分良好的 code modules

像是如果有把專門和外部 service 溝通的 code 抽出來成一個 bean,在進行單元測試時,只要透過 Mockito 更換掉那個 bean 就行了

如果想了解更多 Spring Boot 的用法,也歡迎參考我開設的線上課程 「Java 工程師必備!Spring Boot 零基礎入門」 (輸入折扣碼「HH202501KU」即可享 85 折優惠)

免費訂閱《古古的後端筆記》電子報

每週二學習後端技術,和 2700 人一起變強💪