使用 Mockito 模擬進行 Java 單元測試

什么是 Mockito

官網:https://site.mockito.org/

Mockito is a mocking framework, JAVA-based library that is used for effective unit testing of JAVA applications. Mockito is used to mock interfaces so that a dummy functionality can be added to a mock interface that can be used in unit testing.
Mockito 是一個模擬框架,可以有效地來進行 Java 單元測試鞠呈。Mockito 可以用來模擬接口晾蜘,使得在單元測試中可以使用一個虛構的方法蜀肘。

為什么需要模擬布朦?
單元測試的想法是我們要測試我們的代碼而不測試依賴煞烫。有時候我們不想依靠依賴,或者說依賴沒有準備好猴鲫,此時我們需要模擬对人。

基本用法

  • mock()/@Mock: 創(chuàng)建模擬

    • optionally specify how it should behave via Answer/MockSettings
    • when()/given() 來指定模擬的行為(方法)
    • 默認情況下,調用 mock 對象的帶返回值的方法會返回默認的值拂共,比如返回 null牺弄、0 值或者 false等。
    • 相同的方法和參數唯一確認一個代理宜狐。比如你可以分別代理 get(int) 方法在參數分別為 01 時的不同行為势告。
  • spy()/@Spy: 實現部分模擬, 真正的方法會被調用,但是也可以被 stubbing 和 verify

  • @InjectMocks: 自動注入被 @Spy@Mock 注解的屬性

  • verify(): 驗證方法是否被調用抚恒,調用了幾次

    • 可以使用靈活的匹配參數咱台,例如 any()
    • 也可以通過 @Captor 來捕獲參數

具體參見:https://static.javadoc.io/org.mockito/mockito-core/2.22.0/org/mockito/Mockito.html

在這里通過 maven 進行構建:

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>2.22.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

1. 使用 verify 來驗證行為,例如方法是否被調用

import org.junit.Test;
import java.util.List;
import static org.mockito.Mockito.*;

public class TestRunner {

    @Test
    public void Test1() {
        // 模擬一個接口
        List mockedList = mock(List.class);

        // 使用模擬對象
        mockedList.add("one");
        mockedList.clear();

        // 驗證行為俭驮,方法是否被調用
        verify(mockedList).add("one");
        verify(mockedList).clear();
    }
}

2. 如何使用 stubbing 存根

@Test
public void Test2() {
    // 不光可以模擬接口回溺,可以模擬一個實體類
    LinkedList mockedList = mock(LinkedList.class);

    // stubbing 存根
    when(mockedList.get(0)).thenReturn("first");
    when(mockedList.get(1)).thenThrow(new RuntimeException());

    // 打印 first
    System.out.println(mockedList.get(0));

    // 拋出 java.lang.RuntimeException
    System.out.println(mockedList.get(1));

    // 打印 "null" because get(999) was not stubbed
    System.out.println(mockedList.get(999));
}

3. 參數匹配

例如我們可以使用 anyInt() 來匹配任意的整數類型。
更多的內嵌 matcher 和自定義 matcher表鳍,請參見:https://static.javadoc.io/org.mockito/mockito-core/2.22.0/org/mockito/ArgumentMatchers.html

@Test
public void Test3() {
    // 不光可以模擬接口馅而,可以模擬一個實體類
    LinkedList mockedList = mock(LinkedList.class);

    // stubbing 存根,使用內嵌的 anyInt() 來匹配參數
    when(mockedList.get(anyInt())).thenReturn("element");

    // 打印 element
    System.out.println(mockedList.get(999));

    // 驗證行為譬圣,方法是否被調用
    verify(mockedList).get(anyInt());
}

4. 驗證方法被調用的次數

@Test
public void Test4() {
    // 不光可以模擬接口瓮恭,可以模擬一個實體類
    LinkedList mockedList = mock(LinkedList.class);

    // 使用 mock
    mockedList.add("once");

    mockedList.add("twice");
    mockedList.add("twice");

    mockedList.add("three times");
    mockedList.add("three times");
    mockedList.add("three times");

    // 驗證方法被調用過多少次
    verify(mockedList).add("once");
    verify(mockedList, times(1)).add("once");
    verify(mockedList, times(2)).add("twice");
    verify(mockedList, times(3)).add("three times");

    // 驗證方法沒有被調用過
    verify(mockedList, never()).add("never happened");

    // 驗證方法被調用過多少次
    verify(mockedList, atLeastOnce()).add("three times");
    verify(mockedList, atLeast(2)).add("three times");
    verify(mockedList, atMost(5)).add("three times");
}

5. 驗證方法的調用順序

@Test
public void Test5() {
    List singleMock = mock(List.class);

    // 使用 mock
    singleMock.add("was added first");
    singleMock.add("was added second");

    // 創(chuàng)建 InOrder
    InOrder inOrder = inOrder(singleMock);

    // 驗證先調用 "was added first",再調用 "was added second"
    inOrder.verify(singleMock).add("was added first");
    inOrder.verify(singleMock).add("was added second");
}

6. 使用 @Mock 注解

  • Minimizes repetitive mock creation code. 簡化 Mock 的創(chuàng)建
  • Makes the test class more readable. 增加代碼的可讀性
  • Makes the verification error easier to read because the field name is used to identify the mock.
@Mock List mockedList;

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

@Test
public void Test6() {
    // 使用模擬對象
    mockedList.add("one");
    mockedList.clear();

    // 驗證行為厘熟,方法是否被調用
    verify(mockedList).add("one");
    verify(mockedList).clear();
}

7. 使用 stubbing 存根模擬連續(xù)的調用

@Test
public void Test7() {
    // 模擬一個接口
    List mockedList = mock(List.class);

    when(mockedList.get(anyInt()))
            .thenThrow(new RuntimeException())
            .thenReturn("foo");

    // 第一次調用屯蹦,拋出異常
    mockedList.get(1);

    // 第二次調用,打印 foo
    System.out.println(mockedList.get(1));
}

也可以這樣使用:
when(mock.someMethod("some arg")).thenReturn("one", "two", "three");

8. 使用帶有 callback 回調的 stubbing 存根

@Test
public void Test8() {
    // 模擬一個接口
    List mockedList = mock(List.class);

    when(mockedList.get(anyInt())).thenAnswer(
            new Answer() {
                public Object answer(InvocationOnMock invocation) {
                    Object[] args = invocation.getArguments();
                    Object mock = invocation.getMock();
                    return "called with arguments: " + Arrays.toString(args);
                }
            });

    // 打印 "called with arguments: [1]"
    System.out.println(mockedList.get(1));
}

9. 使用 doReturn()绳姨,doThrow()登澜,doAnswer(),doNothing()飘庄,doCallRealMethod() 來 stub 空方法 void method

@Test
public void Test9() {
    // 模擬一個接口
    List mockedList = mock(List.class);

    doThrow(new RuntimeException()).when(mockedList).clear();

    // 拋出異常 RuntimeException:
    mockedList.clear();
}

10. 在真正的對象上 spy

When you use the spy then the real methods are called (unless a method was stubbed).
當你使用 spy 的時候脑蠕,真正的對象上的方法會被調用,除非你使用了 stubbing跪削,例如 when()...

@Test
public void Test10() {
    List list = new LinkedList();
    List spy = spy(list);

    //using the spy calls *real* methods
    spy.add("one");
    spy.add("two");

    // 打印 one
    System.out.println(spy.get(0));

    // 打印 2 
    System.out.println(spy.size());

    verify(spy).add("one");
    verify(spy).add("two");
}

11. 實現局部模擬

@Test
public void Test11() {
    // 模擬一個接口
    List mockedList = mock(LinkedList.class);

    // 調用實際的方法谴仙,實現局部模擬
    when(mockedList.size()).thenCallRealMethod();

    System.out.println(mockedList.size());
}

12. 重置 Mock

通過 reset(mock); 方法,來重置之前設置的 stubbing碾盐。

示例

假設我們要測試一個計算器程序 CalculatorApplication晃跺,但是該程序依賴于 CalculatorService 實現具體的計算過程。
代碼如下:

public interface CalculatorService {
    public double add(double input1, double input2);

    public double subtract(double input1, double input2);

    public double multiply(double input1, double input2);

    public double divide(double input1, double input2);
}

public class CalculatorApplication {
    private CalculatorService calcService;

    public void setCalculatorService(CalculatorService calcService) {
        this.calcService = calcService;
    }

    public double add(double input1, double input2) {
        return calcService.add(input1, input2);
    }

    public double subtract(double input1, double input2) {
        return calcService.subtract(input1, input2);
    }

    public double multiply(double input1, double input2) {
        return calcService.multiply(input1, input2);
    }

    public double divide(double input1, double input2) {
        return calcService.divide(input1, input2);
    }
}

問題來了:在測試時毫玖,我們可能并沒有 CalculatorService 這個接口的具體實現類掀虎,例如 CalculatorServiceImpl凌盯。
因此我們需要在測試時模擬 CalculatorService 這個接口的行為。

此時我們使用 mockito 來模擬行為烹玉。

mockito 可以通過注解的方式來使用:

  • @RunWith(MockitoJUnitRunner.class):指定 Test Runner
  • @InjectMocks:Mark a field on which injection should be performed. 標識一個變量驰怎,該變量會被注入一個 Mock。例如 CalculatorApplication 會被注入一個 CalculatorService 的實現二打。
    • 注意:CalculatorApplication 中需要定義一個 set 方法來注入砸西。
  • @Mock:Mark a field as a mock. 標識一個變量,該變量會被 Mock址儒。例如 CalculatorService
    • 在標記出 Mock 后衅疙,可以通過 when 來模擬該 Mock 的行為莲趣。

示例如下:

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class Mockito_Test {
    @InjectMocks
    CalculatorApplication calculatorApplication = new CalculatorApplication();

    @Mock
    CalculatorService calcService;

    @Test
    public void testAdd() {
        // 模擬 CalculatorService 的行為
        when(calcService.add(10.0, 20.0)).thenReturn(30.00);

        // 測試
        Assert.assertEquals(calculatorApplication.add(10.0, 20.0), 30.0, 0);
    }
}

Mockito 原理

參考:反模式的經典 - Mockito設計解析

首先我們要知道,Mock 對象這件事情饱溢,本質上是一個 Proxy 模式的應用喧伞。
Proxy 模式說的是,在一個真實對象前面绩郎,提供一個 Proxy 對象潘鲫,所有對真實對象的調用,都先經過 Proxy 對象肋杖,然后由 Proxy 對象根據情況溉仑,決定相應的處理,它可以直接做一個自己的處理状植,也可以再調用真實對象對應的方法
Proxy 對象對調用者來說浊竟,可以是透明的,也可以是不透明的津畸。

Mockito 就是用 Java 提供的 Dynamic Proxy API 來實現的振定。
關于 Java 的動態(tài)代理,請參見 Java 動態(tài)代理

Mockito 本質上就是在代理對象調用方法前肉拓,用 Stubbing 的方式設置其返回值后频,然后在真實調用時,用代理對象返回預設的返回值暖途。

我們來看如下的代碼:

List mockedList = mock(List.class);

// 設置 mock 對象的行為 - 當調用其 get 方法獲取第 0 個元素時卑惜,返回 "first"
when(mockedList.get(0)).thenReturn("first");

Java 中的程序調用是以棧的形式實現的,對于 when() 方法丧肴,mockedList.get(0) 方法的調用對它是不可見的残揉。when() 能接收到的,只有 mockedList.get(0) 的返回值芋浮。
所以抱环,上面的代碼也等價于:

// stubbing 存根
Object ret = mockedList.get(0);
when(ret).thenReturn("first");

看看 when() 方法的源碼:

public <T> OngoingStubbing<T> when(T methodCall) {
    MockingProgress mockingProgress = ThreadSafeMockingProgress.mockingProgress();
    mockingProgress.stubbingStarted();
    OngoingStubbing<T> stubbing = mockingProgress.pullOngoingStubbing();
    if (stubbing == null) {
        mockingProgress.reset();
        throw Reporter.missingMethodInvocation();
    } else {
        return stubbing;
    }
}

看看 OngoingStubbing 接口里有哪些方法:

public interface OngoingStubbing<T> {
    OngoingStubbing<T> thenReturn(T var1);

    OngoingStubbing<T> thenReturn(T var1, T... var2);

    OngoingStubbing<T> thenThrow(Throwable... var1);

    OngoingStubbing<T> thenThrow(Class<? extends Throwable> var1);

    OngoingStubbing<T> thenThrow(Class<? extends Throwable> var1, Class... var2);

    OngoingStubbing<T> thenCallRealMethod();

    OngoingStubbing<T> thenAnswer(Answer<?> var1);

    OngoingStubbing<T> then(Answer<?> var1);

    <M> M getMock();
}

mock 對象所有的方法最終都會交由 MockHandlerImplhandle 方法處理壳快,部分代碼如下:

OngoingStubbingImpl<T> ongoingStubbing = new OngoingStubbingImpl(this.invocationContainer);
ThreadSafeMockingProgress.mockingProgress().reportOngoingStubbing(ongoingStubbing);
StubbedInvocationMatcher stubbing = this.invocationContainer.findAnswerFor(invocation);
StubbingLookupNotifier.notifyStubbedAnswerLookup(invocation, stubbing, this.invocationContainer.getStubbingsAscending(), (CreationSettings)this.mockSettings);
Object ret;
if (stubbing != null) {
    stubbing.captureArgumentsFrom(invocation);

    try {
        ret = stubbing.answer(invocation);
    } finally {
        ThreadSafeMockingProgress.mockingProgress().reportOngoingStubbing(ongoingStubbing);
    }

    return ret;
} else {
    ret = this.mockSettings.getDefaultAnswer().answer(invocation);
    DefaultAnswerValidator.validateReturnValueFor(invocation, ret);
    this.invocationContainer.resetInvocationForPotentialStubbing(invocationMatcher);
    return ret;
}

when 調用的基本形式是 when(mock.doSome()),此時镇草,當 mock.doSome() 時即會觸發(fā)上面的語句眶痰,OngoingStubbingImpl 表示正在對一個方法打樁的包裝,invocationContainerImpl 相當于一個 mock 對象的管家梯啤,記錄著 mock 對象方法的調用竖伯。


引用:
Mockito Tutorial
反模式的經典 - Mockito設計解析
Mockito 源碼解析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市因宇,隨后出現的幾起案子七婴,更是在濱河造成了極大的恐慌,老刑警劉巖察滑,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件打厘,死亡現場離奇詭異,居然都是意外死亡贺辰,警方通過查閱死者的電腦和手機户盯,發(fā)現死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饲化,“玉大人莽鸭,你說我怎么就攤上這事〕钥浚” “怎么了硫眨?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長巢块。 經常有香客問我捺球,道長,這世上最難降的妖魔是什么夕冲? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任氮兵,我火速辦了婚禮,結果婚禮上歹鱼,老公的妹妹穿的比我還像新娘泣栈。我一直安慰自己,他們只是感情好弥姻,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布南片。 她就那樣靜靜地躺著,像睡著了一般庭敦。 火紅的嫁衣襯著肌膚如雪疼进。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天秧廉,我揣著相機與錄音伞广,去河邊找鬼拣帽。 笑死,一個胖子當著我的面吹牛嚼锄,可吹牛的內容都是我干的减拭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼区丑,長吁一口氣:“原來是場噩夢啊……” “哼拧粪!你這毒婦竟也來了?” 一聲冷哼從身側響起沧侥,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤可霎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后宴杀,有當地人在樹林里發(fā)現了一具尸體啥纸,經...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年婴氮,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盾致。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡主经,死狀恐怖,靈堂內的尸體忽然破棺而出庭惜,到底是詐尸還是另有隱情罩驻,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布护赊,位于F島的核電站惠遏,受9級特大地震影響,放射性物質發(fā)生泄漏骏啰。R本人自食惡果不足惜节吮,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望判耕。 院中可真熱鬧透绩,春花似錦、人聲如沸壁熄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽草丧。三九已至狸臣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間昌执,已是汗流浹背烛亦。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工诈泼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人此洲。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓厂汗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親呜师。 傳聞我的和親對象是個殘疾皇子娶桦,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內容