使用 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: 创建模拟

    • optionally specify how it should behave via Answer/MockSettings
    • when()/given() 来指定模拟的行为(方法)
    • 默认情况下,调用 mock 对象的带返回值的方法会返回默认的值,比如返回 null0 值或者 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");

    // 创建 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 的创建
  • 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 存根模拟连续的调用

@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 的动态代理,请参见 Java 动态代理

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() 时即会触发上面的语句,OngoingStubbingImpl 表示正在对一个方法打桩的包装,invocationContainerImpl 相当于一个 mock 对象的管家,记录着 mock 对象方法的调用。


引用:
Mockito Tutorial
反模式的经典 - Mockito设计解析
Mockito 源码解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容