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 就行了