單元測(cè)試 Mockito

一、前言

之前接觸到一次SaaS項(xiàng)目,在進(jìn)行Dubbo接口測(cè)試時(shí),由于某些API是回調(diào)接口通過(guò)MQ交互傳遞消息的,走的異步處理。因?yàn)镸Q在消費(fèi)消息的時(shí)候可能存在延遲,在自動(dòng)化用例調(diào)用接口步驟之后,立即查詢(xún)數(shù)據(jù)庫(kù)結(jié)果可能會(huì)存在消息還在消費(fèi)中數(shù)據(jù)并沒(méi)有落庫(kù),導(dǎo)致斷言case會(huì)失敗,而接口本身定義返回的 message 對(duì)于assert沒(méi)有實(shí)際的意義,這就造成了在assert時(shí)候的麻煩。因?yàn)槟悴恢繫Q消息何時(shí)才消費(fèi)完成并落庫(kù),導(dǎo)致用例的不確定性影響通過(guò)率。剛開(kāi)始的思路是通過(guò)多次遍歷查詢(xún)數(shù)據(jù)庫(kù),但是結(jié)果并不是很理想,因?yàn)楸闅v等待的時(shí)間會(huì)嚴(yán)重影響用例執(zhí)行的效率,通過(guò)率也不符合預(yù)期。

針對(duì)以上問(wèn)題,經(jīng)過(guò)考慮是否能夠采用mock的方式來(lái)解決。上網(wǎng)查詢(xún)相關(guān)資料,發(fā)現(xiàn) Mockito+PowerMockito 框架,提供的相關(guān)API很好的解決了以上問(wèn)題,不得不說(shuō) Mockito 是真的好用,強(qiáng)烈推薦真香!


二、Mockito 入門(mén)

POM依賴(lài)
  <dependencies>
    <!-- https://mvnrepository.com/artifact/org.testng/testng -->
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>7.1.0</version>
      <scope>test</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.powermock/powermock-api-mockito2 -->
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-api-mockito2</artifactId>
      <version>2.0.9</version>
    </dependency>

  </dependencies>
模擬對(duì)象
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.此時(shí)調(diào)用getName方法,會(huì)返回null,因?yàn)檫€沒(méi)有對(duì)方法調(diào)用的返回值做Mock
        System.out.println(person.getName());
模擬對(duì)象返回值
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.Mock 調(diào)用person對(duì)象的getName()方法時(shí),返回angst,在給特定的方法調(diào)用返回固定值在官方說(shuō)法中稱(chēng)為 stub 測(cè)試樁
        when(person.getName()).thenReturn("angst");
        // 3.此時(shí)打印輸出 angst
        System.out.println(person.getName());
模擬指定異常
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.模擬調(diào)用getAge()方法時(shí),拋出 RuntimeException
        when(person.getAge()).thenThrow(new RuntimeException());
        // 3.此時(shí)將會(huì)拋出 RuntimeException
        System.out.println(person.getAge());
為返回值為void的函數(shù)通過(guò)Stub拋出異常
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.用于為無(wú)返回值的函數(shù)打樁
        doThrow(new RuntimeException("Custom Exception")).when(person).setAge(18);
        // 3.調(diào)用這句代碼會(huì)拋出異常
        person.setAge(18);
參數(shù)匹配器 (matchers)
        // 1.Mock Person Object
        LinkedList mockedList = mock(LinkedList.class);
        // 使用內(nèi)置的anyInt()參數(shù)匹配器,anyInt代表任意int
        when(mockedList.get(anyInt())).thenReturn("anyInt");
        //following prints "anyInt"
        System.out.println(mockedList.get(999));
        //you can also verify using an argument matcher
        verify(mockedList).get(anyInt());
驗(yàn)證調(diào)用次數(shù)
        // 1.Mock Person Object
        Person person = mock(Person.class);
        person.setName("amy");
        person.setName("angst");person.setName("angst");
        person.setName("thin bamboo");person.setName("thin bamboo");person.setName("thin bamboo");
        /* 2.下面兩個(gè)寫(xiě)法驗(yàn)證效果一樣,均驗(yàn)證 setName() 方法是否被調(diào)用了一次,verify 函數(shù)默認(rèn)驗(yàn)證的是執(zhí)行了times(1)
        也就是某個(gè)測(cè)試函數(shù)是否執(zhí)行了1次.因此,times(1)通常被省略了,如果 times=2 則拋出異常 TooLittleActualInvocations*/
        verify(person).setName("amy");
        verify(person, times(1)).setName("amy");
        // 3.使用never()進(jìn)行驗(yàn)證,never相當(dāng)于times(0)
        verify(person, never()).setAge(18);
        //atLeastOnce相當(dāng)于times(1)
        verify(person, atLeastOnce()).setName("amy");
        //atLeast至少調(diào)用N次
        verify(person, atLeast(2)).setName("angst");
        //atLeast最多調(diào)用N次
        verify(person, atMost(3)).setName("thin bamboo");
驗(yàn)證執(zhí)行執(zhí)行順序
        // A. Single mock whose methods must be invoked in a particular order
        List singleMock = mock(List.class);
        //using a single mock
        singleMock.add("was added first");
        singleMock.add("was added second");
        //create an inOrder verifier for a single mock
        InOrder inOrder = inOrder(singleMock);
        //following will make sure that add is first called with "was added first, then with "was added second"
        inOrder.verify(singleMock).add("was added first");
        inOrder.verify(singleMock).add("was added second");
        // B. Multiple mocks that must be used in a particular order
        List firstMock = mock(List.class);
        List secondMock = mock(List.class);
        //using mocks
        firstMock.add("was called first");
        secondMock.add("was called second");
        //create inOrder object passing any mocks that need to be verified in order
        InOrder inOrder2 = inOrder(firstMock, secondMock);
        //following will make sure that firstMock was called before secondMock
        inOrder2.verify(firstMock).add("was called first");
        inOrder2.verify(secondMock).add("was called second");
        // Oh, and A + B can be mixed together at will
為連續(xù)的調(diào)用做測(cè)試樁 (stub)
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.第一次調(diào)用返回 "angst",第二次返回"amy",第三次返回"Exception"
        when(person.getName()).thenReturn("angst").thenReturn("amy").thenThrow(new RuntimeException("第三次調(diào)用會(huì)返回異常"));
        System.out.println("第一次調(diào)用:" + person.getName());
        System.out.println("第二次調(diào)用:" + person.getName());
        System.out.println("第三次調(diào)用:" + person.getName());
        // 3.另外,連續(xù)調(diào)用的另一種更簡(jiǎn)短的版本
        when(person.getName()).thenReturn("angst", "amy").thenThrow(new RuntimeException("第三次調(diào)用會(huì)返回異常"));
為回調(diào)做測(cè)試樁
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.運(yùn)行為泛型接口 Answer 打樁
        when(person.getName()).thenAnswer(new Answer() {
            public Object answer(InvocationOnMock invocation) {
                Object[] args = invocation.getArguments();
                Object mock = invocation.getMock();
                return "called with arguments: " + Arrays.toString(args) + " object: "+ mock;
            }
        });
        //Following prints "called with arguments: [] object: Mock for Person, hashCode: 426394307"
        System.out.println(person.getName());
簡(jiǎn)化mock對(duì)象的創(chuàng)建
    @Mock
    private Person person;
    @Mock
    private UserSearchServiceImpl userSearchService;

    //注意!下面這句代碼需要在運(yùn)行測(cè)試函數(shù)之前被調(diào)用,一般放到測(cè)試類(lèi)的基類(lèi)或者test runner中
    @BeforeClass
    public void setUp(){ MockitoAnnotations.initMocks(this); }

三、模擬真實(shí)接口實(shí)戰(zhàn)

1.正常模擬場(chǎng)景

首先來(lái) mock 一個(gè)正常接口方法的返回值。假設(shè)需要測(cè)試一個(gè) UserSearchServiceImpl#getInfo 接口方法,需要對(duì)接口返回修改指定的返回值,然后調(diào)用該接口,最后斷言接口返回。下面看案例:PersonTest.java

package org.example.angst;

import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import static org.mockito.Mockito.*;

public class PersonTest {
    @Mock
    private UserSearchServiceImpl userSearchService;
    private final Person actualResult = new Person("amy", 18, "girls");

    @BeforeClass
    public void before() {
        MockitoAnnotations.initMocks(this);
    }

    /**
     * 首先定義Person類(lèi)
     * 定義一個(gè)UserSearchService接口,定義個(gè)getInfo()方法
     * UserSearchServiceImpl實(shí)現(xiàn)該接口,重寫(xiě)getInfo()方法,返回一個(gè)Person對(duì)象
     */
    @Test(description = "正常mock實(shí)現(xiàn)類(lèi)接口")
    public void testNormalAnalogCall (){
        //1.模擬 getInfo() 方法返回值
        when(userSearchService.getInfo()).thenReturn(new Person("amy", 18, "girls"));
        //2.調(diào)用接口
        Person response = userSearchService.getInfo();
        //3.斷言接口返回和actualResult是否相等
        Assert.assertEquals(response.getName(), actualResult.getName());
    }
}

2.回調(diào)接口場(chǎng)景

本文的重點(diǎn)來(lái)了!有些時(shí)候需要測(cè)試有回調(diào)接口函數(shù),一般來(lái)說(shuō)它們是異步執(zhí)行的。很顯然測(cè)試起來(lái)并不那么輕松,如果使用Thread.sleep(milliseconds)來(lái)等待它們執(zhí)行完成只能說(shuō)是一種比較low的實(shí)現(xiàn),并且會(huì)讓你的測(cè)試具有不確定性。這時(shí)候就體現(xiàn) Mockito 的強(qiáng)大之處.

例:假設(shè)我們有一個(gè)實(shí)現(xiàn)了 DummyCallback 接口的 DummyCallbackImpl,在 DummyCallbackImpl 中有一個(gè)doSomethingAsynchronously()方法,該方法會(huì)調(diào)用構(gòu)造方法中傳入的 DummyCollaborator對(duì)象,并調(diào)用其 doSomethingAsynchronously(DummyCallback callback),而它的任務(wù)在后臺(tái)線(xiàn)程中執(zhí)行完成之后就會(huì)回調(diào)這個(gè)callback 對(duì)象的 onSuccess() 方法。

下面直接看示例:
DummyCallback.java

package org.example.angst;

import java.util.List;

/**
* 提供了兩個(gè)抽象方法
* void onSuccess(List<String> result);
* void onFail(int code);
*/
public interface DummyCallback {
    void onSuccess(List<String> result);
    void onFail(int code);
}

DummyCallbackImpl.java

package org.example.angst;

import java.util.ArrayList;
import java.util.List;

/**
* 實(shí)現(xiàn)了 DummyCallback 接口,構(gòu)造方法入?yún)?DummyCollaborator 對(duì)象
* 提供了doSomethingAsynchronously() 方法默認(rèn)傳入this
*/
public class DummyCallbackImpl implements DummyCallback {

    private final DummyCollaborator dummyCollaborator;
    private  List<String> result = new ArrayList<>();

    public DummyCallbackImpl(DummyCollaborator dummyCollaborator) {
        this.dummyCollaborator = dummyCollaborator;
    }

    public void doSomethingAsynchronously() {
        dummyCollaborator.doSomethingAsynchronously(this);
    }

    public List<String> getResult() {
        return this.result;
    }

    @Override
    public void onSuccess(List<String> result) {
        this.result = result;
        System.out.println("On success");

    }

    @Override
    public void onFail(int code) {
        System.out.println("On Fail"+ code);
    }
}

DummyCollaborator.java

package org.example.angst;

import static java.util.Collections.EMPTY_LIST;

/**
* 異步執(zhí)行操作類(lèi),定義 `doSomethingAsynchronously()` 方法,
* 開(kāi)啟線(xiàn)程回調(diào) `DummyCallback` 對(duì)象的兩個(gè)方法
*/
public class DummyCollaborator {
    public static int ERROR_CODE = 1;

    public void doSomethingAsynchronously (final DummyCallback callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    callback.onSuccess(EMPTY_LIST);
                } catch (InterruptedException e) {
                    callback.onFail(ERROR_CODE);
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
定義測(cè)試類(lèi)

下面會(huì)提供2種不同的方法來(lái)測(cè)試定義好的回調(diào)接口,但是首先我們先創(chuàng)建一個(gè)DummyCollaboratorCallerTest 測(cè)試類(lèi)。

package org.example.angst;

import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.Arrays;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;


public class DummyCollaboratorCallerTest {
    @Mock
    private DummyCallbackImpl dummyCallbackImpl;
    @Mock
    private DummyCollaborator mockDummyCollaborator;
    @Captor
    private ArgumentCaptor<DummyCallback> dummyCallbackArgumentCaptor;

    @BeforeClass
    public void before() {
        MockitoAnnotations.initMocks(this);
        this.dummyCallbackImpl = new DummyCallbackImpl(mockDummyCollaborator);
    }

    @Test
    public void testDoSomethingAsynchronouslyUsingDoAnswer() {}

    @Test
    public void testDoSomethingAsynchronouslyUsingArgumentCaptor() {}
}
doAnswer 測(cè)試回調(diào)接口
    /**
     * 這是我們使用doAnswer()來(lái)為一個(gè)函數(shù)進(jìn)行打樁以測(cè)試異步函數(shù)的測(cè)試用例。這意味著我們需要理解返回一個(gè)回調(diào)(同步的),
     * 當(dāng)被測(cè)試的方法被調(diào)用時(shí)我們生成了一個(gè)通用的 answer,這個(gè)回調(diào)會(huì)被執(zhí)行。
     * 最后,我們調(diào)用了doSomethingAsynchronously函數(shù),并且驗(yàn)證了狀態(tài)和交互結(jié)果。
     */
    @Test(description = "doAnswer 測(cè)試回調(diào)接口")
    public void testDoSomethingAsynchronouslyUsingDoAnswer() {
        // 1.為callback執(zhí)行一個(gè)同步 answer
        final List<String> results = Arrays.asList("One", "Two", "Three");
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                ((DummyCallback)invocation.getArguments()[0]).onSuccess(results);
                return null;
            }
        }).when(mockDummyCollaborator).doSomethingAsynchronously(any(DummyCallback.class));
        // 2.調(diào)用被測(cè)試的函數(shù)
        dummyCallbackImpl.doSomethingAsynchronously();
        // 3.驗(yàn)證狀態(tài)與結(jié)果
        verify(mockDummyCollaborator, times(1)).doSomethingAsynchronously(any(DummyCallback.class));
        Assert.assertEquals(dummyCallbackImpl.getResult(), results);
    }
ArgumentCaptor 測(cè)試異步回調(diào)接口
    /**
     *第二種實(shí)現(xiàn)是使用ArgumentCaptor。在這里我們的callback是異步的: 我們通過(guò)ArgumentCaptor捕獲傳遞到DummyCollaborator對(duì)象的DummyCallback回調(diào)
     * 最終,我們可以在測(cè)試函數(shù)級(jí)別進(jìn)行所有驗(yàn)證,當(dāng)我們想驗(yàn)證狀態(tài)和交互結(jié)果時(shí)可以調(diào)用 onSuccess()
     */
    @Test(description = "ArgumentCaptor 測(cè)試異步回調(diào)接口")
    public void testDoSomethingAsynchronouslyUsingArgumentCaptor() {
        final List<String> results = Arrays.asList("One", "Two", "Three");
        // 1.調(diào)用要被測(cè)試發(fā)函數(shù)
        dummyCallbackImpl.doSomethingAsynchronously();
        // 2.Let's call the callback. ArgumentCaptor.capture() works like a matcher.
        verify(mockDummyCollaborator, times(1)).doSomethingAsynchronously(
                dummyCallbackArgumentCaptor.capture());
        // 3.在執(zhí)行回調(diào)之前驗(yàn)證結(jié)果
        Assert.assertTrue(dummyCallbackImpl.getResult().isEmpty());
        // 4.調(diào)用回調(diào)的onSuccess函數(shù)
        dummyCallbackArgumentCaptor.getValue().onSuccess(results);
        // 5.再次驗(yàn)證結(jié)果
        Assert.assertEquals(dummyCallbackImpl.getResult(), results);
    }

Epilogue

以上兩種實(shí)現(xiàn)的主要的不同點(diǎn)是在當(dāng)使用 DoAnswer() 方案時(shí)我們創(chuàng)建了一個(gè)匿名內(nèi)部類(lèi),并且將它的元素從invocation.getArguments()[n]轉(zhuǎn)換到我們需要的類(lèi)型,當(dāng)萬(wàn)一這個(gè)類(lèi)型匹配失敗,那么對(duì)應(yīng)用例也會(huì)失敗。另一方面,當(dāng)我們使用 ArgumentCaptor 時(shí)我們可能能夠更精準(zhǔn)的控制測(cè)試用例,因?yàn)槲覀兡軌虿东@mock對(duì)象,并且能夠通過(guò)手動(dòng)來(lái)操作回調(diào)對(duì)象。以上兩種方式雖然都能實(shí)現(xiàn)回調(diào)接口的mock,但是我更傾向于第二種。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容