Mockito 使用指南 - 单元测试利器

Mock 是什么

mock 测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。

简单的看一张图

我们在测试类 A 时,类 A 需要调用类 B 和类 C,而类 B 和类 C 又需要调用其他类如 D、E、F 等,假如类 D、E、F 构造很耗时又或者调用很耗时的话是非常不便于测试的(比如是 DAO 类,每次访问数据库都很耗时)。所以我们引入 Mock 对象。

如上图,我们将类 B 和类 C 替换成 Mock 对象,在调用类 B 和类 C 的方法时,用 Mock 对象的方法来替换(当然我们要自己设定参数和期望结果)而不用实际去调用其他类。这样测试效率会高很多。

一句话为什么要 Mock:因为我们实际编写程序都不会是一个简单类,而是有着复杂依赖关系的类,Mock 对象让我们在不依赖具体对象的情况下完成测试。

Mockito就是一个优秀的用于单元测试的mock框架。Mockito已经在github上开源,详细请点击:https://github.com/mockito/mockito

除了Mockito以外,还有一些类似的框架,比如:

  • EasyMock:早期比较流行的MocK测试框架。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常

  • PowerMock:这个工具是在EasyMock和Mockito上扩展出来的,目的是为了解决EasyMock和Mockito不能解决的问题,比如对static, final, private方法均不能mock。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了

  • JMockit:JMockit 是一个轻量级的mock框架是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode

Mockito已经被广泛应用,所以这里重点介绍Mockito。

Mock 的关键点

Mock 对象

模拟对象的概念就是我们想要创建一个可以替代实际对象的对象,这个模拟对象要可以通过特定参数调用特定的方法,并且能返回预期结果。

Stub(桩)

桩指的是用来替换具体功能的程序段。桩程序可以用来模拟已有程序的行为或是对未完成开发程序的一种临时替代。

设置预期

通过设置预期明确 Mock 对象执行时会发生什么,比如返回特定的值、抛出一个异常、触发一个事件等,又或者调用一定的次数。

验证预期的结果

设置预期和验证预期是同时进行的。设置预期在调用测试类的函数之前完成,验证预期则在它之后。所以,首先你设定好预期结果,然后去验证你的预期结果是否正确。

Mock 的好处是什么

提前创建测试; TDD(测试驱动开发)

这是个最大的好处吧。如果你创建了一个Mock那么你就可以在service接口创建之前写Service Tests了,这样你就能在开发过程中把测试添加到你的自动化测试环境中了。换句话说,模拟使你能够使用测试驱动开发。

团队可以并行工作

这类似于上面的那点;为不存在的代码创建测试。但前面讲的是开发人员编写测试程序,这里说的是测试团队来创建。当还没有任何东西要测的时候测试团队如何来创建测试呢?模拟并针对模拟测试!这意味着当service借口需要测试时,实际上QA团队已经有了一套完整的测试组件;没有出现一个团队等待另一个团队完成的情况。这使得模拟的效益型尤为突出了。

你可以创建一个验证或者演示程序

由于Mocks非常高效,Mocks可以用来创建一个概念证明,作为一个示意图,或者作为一个你正考虑构建项目的演示程序。这为你决定项目接下来是否要进行提供了有力的基础,但最重要的还是提供了实际的设计决策。

为无法访问的资源编写测试

这个好处不属于实际效益的一种,而是作为一个必要时的“救生圈”。有没有遇到这样的情况?当你想要测试一个service接口,但service需要经过防火墙访问,防火墙不能为你打开或者你需要认证才能访问。遇到这样情况时,你可以在你能访问的地方使用MockService替代,这就是一个“救生圈”功能。

Mock 可以交给用户

在有些情况下,某种原因你需要允许一些外部来源访问你的测试系统,像合作伙伴或者客户。这些原因导致别人也可以访问你的敏感信息,而你或许只是想允许访问部分测试环境。在这种情况下,如何向合作伙伴或者客户提供一个测试系统来开发或者做测试呢?最简单的就是提供一个mock,无论是来自于你的网络或者客户的网络。soapUI mock非常容易配置,他可以运行在soapUI或者作为一个war包发布到你的java服务器里面。

隔离系统

有时,你希望在没有系统其他部分的影响下测试系统单独的一部分。由于其他系统部分会给测试数据造成干扰,影响根据数据收集得到的测试结论。使用mock你可以移除掉除了需要测试部分的系统依赖的模拟。当隔离这些mocks后,mocks就变得非常简单可靠,快速可预见。这为你提供了一个移除了随机行为,有重复模式并且可以监控特殊系统的测试环境。

Mockito 使用举例

首先引入依赖配置,gradle或maven都行。

repositories { jcenter() }
dependencies { testCompile "org.mockito:mockito-core:2.+" }

我的部分gradle配置如下

repositories {
   mavenCentral()
   jcenter()
}

dependencies {
   compile('org.springframework.boot:spring-boot-starter-web')
   testCompile('org.springframework.boot:spring-boot-starter-test')
   testCompile('org.mockito:mockito-core:2.+')
}

我的GitHub源码:https://github.com/X-rapido/Mockito_Demo

这里我们直接通过一个代码来说明mockito对单元测试的帮助,代码有三个类,分别如下: 

Person类:

package com.example.bean;

public class Person {
    private final int id;
    private final String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

PersonDao

package com.example.dao;

import com.example.bean.Person;

public interface PersonDao {
    Person getPerson(int id);
    boolean update(Person person);
}

PersonService

package com.example.service;

import com.example.bean.Person;
import com.example.dao.PersonDao;

public class PersonService {

    private final PersonDao personDao;

    public PersonService(PersonDao personDao) {
        this.personDao = personDao;
    }

    public boolean update(int id, String name) {
        Person person = personDao.getPerson(id);
        if (person == null) {
            return false;
        }

        Person personUpdate = new Person(person.getId(), name);
        return personDao.update(personUpdate);
    }
}

在这里,我们要进行测试的是PersonService类的update方法,我们发现,update方法依赖PersonDAO,在开发过程中,PersonDAO很可能尚未开发完成,所以我们测试PersonService的时候,所以该怎么测试update方法呢?连接口都还没实现,怎么知道返回的是true还是false?

在这里,我们可以这样认为,单元测试的思路就是我们想在不涉及依赖关系的情况下测试代码。这种测试可以让你无视代码的依赖关系去测试代码的有效性。核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常。所以我们的做法是mock一个PersonDAO对象,至于实际环境中,PersonDAO行为是否能按照预期执行,比如update是否能成功,查询是否返回正确的数据,就跟PersonService没关系了。PersonService的单元测试只测试自己的逻辑是否有问题

下面编写测试代码:

package com.example;

import com.example.bean.Person;
import com.example.dao.PersonDao;
import com.example.service.PersonService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import static org.mockito.Mockito.*;

public class PersonServiceTest2 {

    private PersonDao mockDao;
    private PersonService personService;

    @Before
    public void setUp() {
        // 模拟PersonDao对象
        mockDao = mock(PersonDao.class);
        when(mockDao.getPerson(1)).thenReturn(new Person(1, "Person1"));
        when(mockDao.update(isA(Person.class))).thenReturn(true);

        personService = new PersonService(mockDao);
    }

    @Test
    public void testUpdate() {
        boolean result = personService.update(1, "new name");
        Assert.assertTrue("must true", result);

        //验证是否执行过一次getPerson(1)
        verify(mockDao, times(1)).getPerson(eq(1));

        // 验证是否执行过一次update
        verify(mockDao, times(1)).update(isA(Person.class));
    }

    @Test
    public void testUpdateNotFind(){
        boolean result = personService.update(2, "new name");
        Assert.assertFalse("must true", result);

        // 验证是否执行过一次getPerson(1)
        verify(mockDao, times(1)).getPerson(eq(1));

        //验证是否执行过一次update
        verify(mockDao, never()).update(isA(Person.class));
    }

}

我们对PersonDAO进行mock,并且设置stubbing,stubbing设置如下:

  • 当getPerson方法传入1的时候,返回一个Person对象,否则默认返回空

  • 当调update方法的时候,返回true

我们验证了两种情况:

  • 更新id为1的Person的名字,预期:能在DAO中找到Person并更新成功

  • 更新id为2的Person的名字,预期:不能在DAO中找到Person,更新失败

这样,根据PersonService的update方法的逻辑,通过这两个test case之后,我们认为代码是没有问题的。mockito在这里扮演了一个为我们模拟DAO对象,并且帮助我们验证行为(比如验证是否调用了getPerson方法及update方法)的角色

Mockito使用方法

Mockito的使用,有详细的api文档,具体可以查看:http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html,下面是整理的一些常用的使用方式。

验证行为

一旦创建,mock会记录所有交互,你可以验证所有你想要验证的东西

/**
 * 验证行为
 */
@Test
public void testVerify(){
    // mock creation
    List mockedList = mock(List.class);

    // using mock object
    mockedList.add("one");
    mockedList.add("two");
    mockedList.add("two");
    mockedList.clear();

    // verification
    //验证是否调用过一次 mockedList.add("one")方法,若不是(0次或者大于一次),测试将不通过
    verify(mockedList).add("one");

    // 验证调用过2次 mockedList.add("two")方法,若不是,测试将不通过
    verify(mockedList, times(2)).add("two");

    // 验证是否调用过一次 mockedList.clear()方法,若没有(0次或者大于一次),测试将不通过
    verify(mockedList).clear();
}

Stubbing

/**
 * Stubbing
 */
@Test
public void testStubbing(){
    // 你可以mock具体的类,而不仅仅是接口
    LinkedList mockedList = mock(LinkedList.class);

    // 设置桩
    when(mockedList.get(0)).thenReturn("first");
    when(mockedList.get(1)).thenThrow(new RuntimeException());

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

    // 这里会抛runtime exception
    // System.out.println(mockedList.get(1));

    // 这里会打印 "null" 因为 get(999) 没有设置
    System.out.println(mockedList.get(999));

    // Although it is possible to verify a stubbed invocation, usually it's just redundant
    // If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
    // If your code doesn't care what get(0) returns, then it should not be stubbed. Not convinced? See here.
    verify(mockedList).get(0);
}

对于stubbing,有以下几点需要注意:

  • 对于有返回值的方法,mock会默认返回null、空集合、默认值。比如,为int/Integer返回0,为boolean/Boolean返回false

  • stubbing可以被覆盖,但是请注意覆盖已有的stubbing有可能不是很好

  • 一旦stubbing,不管调用多少次,方法都会永远返回stubbing的值

  • 当你对同一个方法进行多次stubbing,最后一次stubbing是最重要的

参数匹配

/**
 * 参数匹配
 */
@Test
public void testArgumentMatcher() {
    LinkedList mockedList = mock(LinkedList.class);

    // 用内置的参数匹配器来stub
    when(mockedList.get(anyInt())).thenReturn("element");

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

    // 你也可以用参数匹配器来验证,此处测试通过
    verify(mockedList).get(anyInt());

    // 此处测试将不通过,因为没调用get(33)
    verify(mockedList).get(eq(33));
}

如果你使用了参数匹配器,那么所有参数都应该使用参数匹配器

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//上面是正确的,因为eq返回参数匹配器

verify(mock).someMethod(anyInt(), anyString(), "third argument");
//上面将会抛异常,因为第三个参数不是参数匹配器,一旦使用了参数匹配器来验证,那么所有参数都应该使用参数匹配

验证准确的调用次数,最多、最少、从未等

/**
 * 验证准确的调用次数,最多、最少、从未等
 */
@Test
public void testInvocationTimes() {
    LinkedList mockedList = mock(LinkedList.class);

    // using mock
    mockedList.add("once");

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

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

    // 下面两个是等价的, 默认使用times(1)
    verify(mockedList).add("once");
    verify(mockedList, times(1)).add("once");

    // 验证准确的调用次数
    verify(mockedList, times(2)).add("twice");
    verify(mockedList, times(3)).add("three times");

    // 从未调用过. never()是times(0)的别名
    verify(mockedList, never()).add("never happened");

    // 用atLeast()/atMost()验证
    verify(mockedList, atLeastOnce()).add("three times");

    // 下面这句将不能通过测试
    verify(mockedList, atLeast(2)).add("five times");
    verify(mockedList, atMost(5)).add("three times");
}

为void方法抛异常

/**
 * 为void方法抛异常
 */
@Test
public void testVoidMethodsWithExceptions() {
    LinkedList mockedList = mock(LinkedList.class);
    doThrow(new RuntimeException()).when(mockedList).clear();

    // 当调用clear方法时,抛RuntimeException异常
    mockedList.clear();
}

验证调用顺序

/**
 * 验证调用顺序
 */
@Test
public void testVerificationInOrder() {
    // A. 单一模拟的方法必须以特定顺序调用
    List singleMock = mock(List.class);

    // 使用单个mock对象
    singleMock.add("was added first");
    singleMock.add("was added second");

    // 创建inOrder
    InOrder inOrder = inOrder(singleMock);

    // 验证调用次数,若是调换两句,将会出错,因为singleMock.add("was added first")是先调用的
    inOrder.verify(singleMock).add("was added first");
    inOrder.verify(singleMock).add("was added second");

    // 多个mock对象
    List firstMock = mock(List.class);
    List secondMock = mock(List.class);

    // using mocks
    firstMock.add("was called first");
    secondMock.add("was called second");

    // 创建多个mock对象的inOrder
    inOrder = inOrder(firstMock, secondMock);

    // 验证firstMock先于secondMock调用
    inOrder.verify(firstMock).add("was called first");
    inOrder.verify(secondMock).add("was called second");
}

验证mock对象没有产生过交互

/**
 * 验证mock对象没有产生过交互
 */
@Test
public void testInteractionNeverHappened() {
    List mockOne = mock(List.class);
    List mockTwo = mock(List.class);

    // 测试通过
    verifyZeroInteractions(mockOne, mockTwo);

    mockOne.add("");

    // 测试不通过,因为mockTwo已经发生过交互了
    verifyZeroInteractions(mockOne, mockTwo);
}

查找是否有未验证的交互

不建议过多使用,api原文:A word of warning: Some users who did a lot of classic, expect-run-verify mocking tend to use verifyNoMoreInteractions() very often, even in every test method. verifyNoMoreInteractions() is not recommended to use in every test method. verifyNoMoreInteractions() is a handy assertion from the interaction testing toolkit. Use it only when it’s relevant. Abusing it leads to overspecified, less maintainable tests.

/**
 * 查找是否有未验证的交互
 */
@Test
public void testFindingRedundantInvocations() throws Exception {
    List mockedList = mock(List.class);

    //using mocks
    mockedList.add("one");
    mockedList.add("two");

    verify(mockedList).add("one");

    //验证失败,因为mockedList.add("two")尚未验证
    verifyNoMoreInteractions(mockedList);
}

@Mock注解

  • 减少代码

  • 增强可读性

  • 让verify出错信息更易读,因为变量名可用来描述标记mock对象

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.List;

import static org.mockito.Mockito.*;

public class MockTest {

    @Mock
    List<String> mockedList;

    @Before
    public void initMocks() {

        //必须,否则注解无效
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testMock() {
        mockedList.add("one");
        verify(mockedList).add("one");
    }
}

根据调用顺序设置不同的stubbing

public interface MockInterfaceTest {
    String someMethod(String arg);
}

/**
 * 根据调用顺序设置不同的stubbing
 */
@Test
public void testStubbingConsecutiveCalls() {

    MockInterfaceTest mock = mock(MockInterfaceTest.class);
    when(mock.someMethod("some arg")).thenThrow(new RuntimeException("")).thenReturn("foo");
//  when(mock.someMethod("some arg")).thenReturn("aaa").thenReturn("foo");

    // 第一次调用,抛RuntimeException
    mock.someMethod("some arg");

    // 第二次调用返回foo
    System.out.println(mock.someMethod("some arg"));

    // 后续继续调用,返回“foo”,以最后一个stub为准
    System.out.println(mock.someMethod("some arg"));

    // 下面是一个更简洁的写法
    when(mock.someMethod("some arg")).thenReturn("one", "two", "three");
    System.out.println(mock.someMethod("some arg"));    // one
    System.out.println(mock.someMethod("some arg"));    // two
    System.out.println(mock.someMethod("some arg"));    // three
}

spy监视真正的对象

  • spy是创建一个拷贝,如果你保留原始的list,并用它来进行操作,那么spy并不能检测到其交互

  • spy一个真正的对象+试图stub一个final方法,这样是会有问题的

/**
 * spy监视真正的对象
 */
@Test
public void testSpy() {
    List list = new LinkedList();
    List spy = spy(list);

    // 可选的,你可以stub某些方法
    when(spy.size()).thenReturn(100);

    // 调用"真正"的方法
    spy.add("one");
    spy.add("two");

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

    // size()方法被stub了,打印100
    System.out.println(spy.size());

    // 可选,验证spy对象的行为
    verify(spy).add("one");
    verify(spy).add("two");

    //下面写法有问题,spy.get(10)会抛IndexOutOfBoundsException异常
    when(spy.get(10)).thenReturn("foo");

    // 可用以下方式
    doReturn("foo").when(spy).get(10);
}

为未stub的方法设置默认返回值

/**
 * 为未stub的方法设置默认返回值
 */
@Test
public void testDefaultValue() {

    List listOne = mock(List.class, Mockito.RETURNS_SMART_NULLS);

    List listTwo = mock(List.class, new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {

            // TODO: return default value here
            return null;
        }
    });

    // lambda表达式写法
    List listThree = mock(List.class, (invocation)-> "听风");

    System.out.println(listOne.get(10));
    System.out.println(listTwo.get(10));
    System.out.println(listThree.get(10));
}

参数捕捉

/**
 * 参数捕捉
 */
@Test
public void testCapturingArguments() {
    List mockedList = mock(List.class);

    ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
    mockedList.add("John");

    // 验证后再捕捉参数
    verify(mockedList).add(argument.capture());

    // 验证参数
    Assert.assertEquals("John", argument.getValue());
}

重置mocks

Don’t harm yourself. reset() in the middle of the test method is a code smell (you’re probably testing too much).

/**
 * 重置mocks
 */
@Test
public void testReset() {
    List mock = mock(List.class);
    when(mock.size()).thenReturn(10);
    mock.add(1);
    reset(mock);
    //从这开始,之前的交互和stub将全部失效
}

Serializable mocks

WARNING: This should be rarely used in unit testing.

@Test
public void testSerializableMocks() throws Exception {
    List serializableMock = mock(List.class, withSettings().serializable());
}

更多的注解:@Captor, @Spy, @InjectMocks

  • @Captor 创建ArgumentCaptor

  • @Spy 可以代替spy(Object).

  • @InjectMocks 如果此注解声明的变量需要用到mock对象,mockito会自动注入mock或spy成员

//可以这样写
@Spy BeerDrinker drinker = new BeerDrinker();

//也可以这样写,mockito会自动实例化drinker
@Spy BeerDrinker drinker;

//会自动实例化
@InjectMocks LocalPub;

超时验证

/**
 * 超时验证
 */
@Test
public void testTimeout(){
    TimeMockTest mock = mock(TimeMockTest.class);

    // 测试程序将会在下面这句阻塞100毫秒,timeout的时候再进行验证是否执行过someMethod()
    verify(mock, timeout(100)).someMethod();

    // 和上面代码等价
    verify(mock, timeout(100).times(1)).someMethod();

    // 阻塞100ms,timeout的时候再验证是否刚好执行了2次
    verify(mock, timeout(100).times(2)).someMethod();

    // timeout的时候,验证至少执行了2次
    verify(mock, timeout(100).atLeast(2)).someMethod();

    // timeout时间后,用自定义的检验模式验证someMethod()
    VerificationMode yourOwnVerificationMode = new VerificationMode() {

        @Override
        public void verify(VerificationData data) {
            System.out.println(data);
        }

        @Override
        public VerificationMode description(String description) {
            System.out.println(description);
            return null;
        }
    };

    verify(mock, new Timeout(100, yourOwnVerificationMode)).someMethod();
}

查看是否mock或者spy

/**
 * 查看是否mock或者spy
 */
@Test
public void testMockAndSpy(){
    TimeMockTest mock = mock(TimeMockTest.class);

    System.out.println(Mockito.mockingDetails(mock).isMock());  // true
    System.out.println(Mockito.mockingDetails(mock).isSpy());   // false
}

参考文档

Mockito官网:http://site.mockito.org

Mockito API:http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html

Mockito项目源码:https://github.com/mockito/mockito

Mockito使用指南:https://blog.csdn.net/shensky711/article/details/52771493

Mock 模拟测试简介及 Mockito 使用入门:https://www.2cto.com/kf/201607/528417.html

单元测试利器-Mockito 中文文档:https://blog.csdn.net/bboyfeiyu/article/details/52127551

赞(52) 打赏
未经允许不得转载:优客志 » JAVA开发
分享到:

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏