Mockito 簡介


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

什么是 Mock 測試?

Mock 測試就是在測試過程中,創建一個假的對象,避免你為了測試一個方法,卻要自行構建整個 Bean 的依賴鏈。

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

img

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

img

Mockito 簡介

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

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

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

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

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

官方

入門:5分鍾了解Mockito

Mockito:一個強大的用於 Java 開發的模擬測試框架

Mockito 簡明教程

在 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){

    	returnuserDao.getUserById(id);

    }

    publicInteger insertUser(User user){

    	returnuserDao.insertUser(user);

    }

}

User Model 的定義如下:

public class User {

    privateInteger id;

    privateString name;

    //省略 getter/setter

}

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

@RunWith(SpringRunner.class)

@SpringBootTest

public class UserServiceTest {

    //先普通的注入一個userService bean

    @Autowired

    private UserService userService;

    @Test

    public void getUserById() throws Exception {

        //普通的使用userService,他里面會再去調用userDao取得數據庫的數據

        User user = userService.getUserById( 1);

        //檢查結果

        Assert.assertNotNull(user);

        Assert.assertEquals(user.getId, newInteger( 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 自定義方法的返回值,這里有一個公式用法,下面這段代碼的意思為,當調用了某個 Mock 對象的方法時,就回傳我們想要的自定義結果。

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

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

@RunWith(SpringRunner.class)

@SpringBootTest

publicclass 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( newUser( 200, "I'm mock 3"));

    // 返回的會是名字為I'm mock 3的user對象

    User user = userService.getUserById( 1);

    Assert.assertNotNull(user);

    Assert.assertEquals(user.getId, newInteger( 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( newUser( 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( newUser( 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( newUser); //會返回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));

Spring中mock任何容器內對象

Spring中正常使用mockito

上demo代碼:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:testApplicationContext.xml" })
public class SpringMockitoTest {

    @Mock
    private ApiService mockApiService;

    @Before
    public void initMocks() {
        MockitoAnnotations.initMocks(this);
        when(mockApiService.test()).thenReturn("ok");
    }

    @Test
    public void should_success_when_testApiService() {
        String result = mockApiService.test();
        Assert.assertEquals("ok", result);
    }
}

@Component
public class ApiService {

    @Autowired
    private TestApiService testApiService;

    public String test() {
        String connect = testApiService.connect();
        connect += "test";//test自己的業務
        return connect;
    }
}

@Component
public class TestApiService {
    public String connect() {
        return "error";
    }

    public String  findFromDb() {
        return "db_data";
    }
}

正常使用spring和mockito中,我們把需要的mock的ApiService給mock掉,但是我們更想的是把TestApiService中的connect方法mock掉,這樣就可以測試我們自己的代碼,也就是ApiService中test方法自己的業務。

Spring中mock任何容器內對象

上面的demo中,我們如何mock掉TestApiService中的test方法?

因為TestApiService是spring容器管理的bean,並且ApiService中使用到TestApiService,所以我們把ApiService中引用的TestApiService替換成我們的mock對象即可。

Spring框架有個反射工具ReflectionTestUtils可以把一個對象中屬性設置為新值,我們可以使用:

ReflectionTestUtils.setField(apiService, "testApiService", spyTestApiService);

把我們mock的testApiService放到apiService中,這樣apiService調用就是我們mock的對象了;但是默認spring中apiService對象是代理對象,不能直接把值設置到屬性上,所以我們自己寫個小的工具類,在最后如下:

ReflectionTestUtils.setField(AopTargetUtils.getTarget(apiService), "testApiService", spyTestApiService);

完整demo:

 @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:testApplicationContext.xml" })
public class SpringMockitoTest {

    @Autowired
    private ApiService apiService;
    @Mock
    private TestApiService spyTestApiService;
    @Autowired
    private TestApiService testApiService;

    @Before
    public void initMocks() throws Exception {
        MockitoAnnotations.initMocks(this);
        ReflectionTestUtils.setField(AopTargetUtils.getTarget(apiService), "testApiService", spyTestApiService);
        when(spyTestApiService.connect()).thenReturn("ok");
    }

    @After
    public void clearMocks() throws Exception {
        ReflectionTestUtils.setField(AopTargetUtils.getTarget(apiService), "testApiService", testApiService);
    }

    @Test
    public void should_success_when_testApiService() {
        String result = apiService.test();
        Assert.assertEquals("oktest", result);
    }
}

@Component
public class ApiService {

    @Autowired
    private TestApiService testApiService;

    public String test() {
        String connect = testApiService.connect();
        connect += "test";//test自己的業務
        return connect;
    }
}

@Component
public class TestApiService {
    public String connect() {
        return "error";
    }

    public String  findFromDb() {
        return "db_data";
    }
}

public class AopTargetUtils {
    /**
     * 獲取 目標對象
     * @param proxy 代理對象
     * @return
     * @throws Exception
     */
    public static Object getTarget(Object proxy) throws Exception {
        if(!AopUtils.isAopProxy(proxy)) {
            return proxy;//不是代理對象
        }
        if(AopUtils.isJdkDynamicProxy(proxy)) {
            return getJdkDynamicProxyTargetObject(proxy);
        } else { //cglib
            return getCglibProxyTargetObject(proxy);
        }
    }

    private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
        h.setAccessible(true);
        Object dynamicAdvisedInterceptor = h.get(proxy);
        Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
        advised.setAccessible(true);
        Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
        return target;
    }


    private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
        h.setAccessible(true);
        AopProxy aopProxy = (AopProxy) h.get(proxy);
        Field advised = aopProxy.getClass().getDeclaredField("advised");
        advised.setAccessible(true);
        Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();
        return target;
    }
}

最后就是注意測試之后要還原現場,把spring對象還原,尤其在跑maven test的時候,否則可能會影響其他人的測試。

Mockito 的限制

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

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

因此在寫代碼時,需要做良好的功能拆分,才能夠使用 Mockito 的 Mock 技術,幫助我們降低測試時 Bean 的耦合度。

總結

Mockito 是一個非常強大的框架,可以在執行單元測試時幫助我們模擬一個 Bean,提高單元測試的穩定性。

並且大家可以嘗試在寫代碼時,從 Mock 測試的角度來寫,更能夠寫出功能切分良好的代碼架構,像是如果有把專門和外部服務溝通的代碼抽出來成一個 Bean,在進行單元測試時,只要透過 Mockito 更換掉那個 Bean 就行了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM