Mock工具之Mockito实战

Stella981
• 阅读 949

在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。为了解决这类问题我们引入了Mock的概念,简单的说就是模拟这些需要构建的类或者资源,提供给需要测试的对象使用。业内的Mock工具有很多,也已经很成熟了,这里我们将直接使用最流行的Mockito进行实战演练,完成mockito教程。

Mock工具概述

1.1  Mockito简介

Mock工具之Mockito实战

EasyMock 以及 Mockito 都因为可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中,但是这两种 Mock 工具都不可以实现对静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟,但是这些方法往往是我们在大型系统中需要的功能。

另外,关于更多Mockito2.0新特性,参考官方介绍文档,里边有关于为什么不mock private的原因,挺有意思的:

https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2

1.2 Mockito准备工作

###Maven###

通过Maven管理的,需要在项目的Pom.xml中增加如下的依赖:

Mock工具之Mockito实战

<dependencies>

<dependency>

<groupId>org.mockito</groupId>

<artifactId>mockito-core</artifactId>

<version>2.7.19</version>

<scope>test</scope>

</dependency>

</dependencies>

Mock工具之Mockito实战

在程序中可以import org.mockito.Mockito,然后调用它的static方法。

Maven用户可以声明对mockito-core的依赖。 Mockito自动发布到Bintray的中心,并同步到Maven Central Repository。

特别提醒:使用手工依赖关系管理的Legacy构建可以使用1. *“mockito-all”分发。 它可以从Mockito的Bintray存储库或Bintray的中心下载。 在但是Mockito 2. * “mockito-all”发行已经停止,Mockito 2以上版本使用“mockito-core”。

官网下载中心:

http://search.maven.org/#search|gav|1|g%3A%22org.mockito%22%20AND%20a%3A%22mockito-core%22

目前最新版本为2.7.19,由于公司网络网关问题,最好是去官网手工下载。

Mock工具之Mockito实战

另外Mockito需要Junit配合使用,在Pom文件中同样引入:

Mock工具之Mockito实战

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

Mock工具之Mockito实战

然后为了使代码更简洁,最好在测试类中导入静态资源,还有为了使用常用的junit关键字,也要引入junit的两个类Before和Test:

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;

1.3 模拟对象

创建 Mock 对象的语法为 mock(class or interface)。

Mock 对象的创建

mock(Class classToMock);
 mock(Class classToMock, String name)
 mock(Class classToMock, Answer defaultAnswer)
 mock(Class classToMock, MockSettings mockSettings)
 mock(Class classToMock, ReturnValues returnValues)

可以对类和接口进行mock对象的创建,创建时可以为mock对象命名。对mock对象命名的好处是调试的时候容易辨认mock对象。

Mock对象的期望行为和返回值设定

假设我们创建了LinkedList类的mock对象:

 LinkedList mockedList = mock(LinkedList.class);

1.4 设置对象调用的预期返回值

通过 when(mock.someMethod()).thenReturn(value) 来设定 Mock 对象某个方法调用时的返回值。我们可以看看源码中关于thenReturn方法的注释:

Mock工具之Mockito实战

使用when(mock.someMethod()).thenThrow(new RuntimeException) 的方式来设定当调用某个方法时抛出的异常。

Mock工具之Mockito实战

以及Answer:

Mock工具之Mockito实战

Answer 是个泛型接口。到调用发生时将执行这个回调,通过  Object[] args = invocation.getArguments();可以拿到调用时传入的参数,通过 Object mock = invocation.getMock();可以拿到mock对象。

有些方法可能接口的参数为一个Listener参数,如果我们使用Answer打桩,我们就可以获取这个Listener,并且在Answer函数中执行对应的回调函数,这对我们了解函数的内部执行过成有很大的帮助。

使用doThrow(new RuntimeException(“clear exception”)).when(mockedList).clear();mockedList.clear();的方式Mock没有返回值类型的函数:

Mock工具之Mockito实战

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

//将会 抛出 RuntimeException:

mockedList.clear();

这个实例表示当执行到mockedList.clear()时,将会抛出RuntimeException。其他的doXXX执行与它类似。

例如 : doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod() 系列方法。

Spy函数:

你可以为真实对象创建一个监控(spy)对象,当你使用这个spy对象时,真实的对象也会被调用,除非它的函数被打桩。你应该尽量少的使用spy对象,使用时也需要小心,例如spy对象可以用来处理遗留代码,Spy示例如下:

Mock工具之Mockito实战

List list = new LinkedList();

//监控一个真实对象

List spy = spy(list);

//你可以为某些函数打桩

when(spy.size()).thenReturn(100);

//使用这个将调用真实对象的函数

spy.add("one");

spy.add("two");

//打印"one"

System.out.println(spy.get(0));

//size() 将打印100

System.out.println(spy.size());

//交互验证

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

Mock工具之Mockito实战

理解监控真实对象非常重要,有时,在监控对象上使用when(Object)来进行打桩是不可能或者不切实际的。因为,当使用监控对象时,请考虑用doReturn、Answer、Throw()函数组来进行打桩,例如:

List list = new LinkedList();

List spy = spy(list);

//这是不可能的: 因为调用spy.get(0)时会调用真实对象的get(0)函数,此时会发生

//IndexOutOfBoundsException异常,因为真实对象是空的 when(spy.get(0)).thenReturn("foo");

//你需要使用 doReturn() 来打桩

doReturn("foo").when(spy).get(0);

Mockito并不会为真实的对象代理函数调用,实际上它会复制真实对象,因此,如果你保留了真实对象并且与之交互,不要期望监控对象得到正确的结果。当你在监控对象上调用一个没有stub函数时,并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果。

1.5 验证被测试类方法

Mock 对象一旦建立便会自动记录自己的交互行为,所以我们可以有选择的对它的 交互行为进行验证。在 Mockito 中验证 Mock 对象交互行为的方法是 verify(mock).someMethod(…)。最后 Assert() 验证返回值是否和预期一样。

1.6 Demo

 从网上找来一个最简单的代码实例,下面以具体代码演示如何使用Mockito,代码有三个类,分别如下:

Person类:

Mock工具之Mockito实战

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; 
        }
}

Mock工具之Mockito实战

PersonDao类:

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

PersonService类:

Mock工具之Mockito实战

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); 
        }
}

Mock工具之Mockito实战

仍然使用Junit自动生成测试类或者手工新建测试类:

Mock工具之Mockito实战

测试代码生成后,将默认assertfail的删掉,输入以下两个测试方法:

Mock工具之Mockito实战

import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;


public class PersonServiceTest {

    private PersonDao     mockDao;
    private PersonService personService;

    @Before
    public void setUp() throws Exception {
        //模拟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() throws Exception {
        boolean result = personService.update(1, "new name");
        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() throws Exception {
        boolean result = personService.update(2, "new name");
        assertFalse("must true", result);
        //验证是否执行过一次getPerson(1)
        verify(mockDao, times(1)).getPerson(eq(1));
        //验证是否执行过一次update
        verify(mockDao, never()).update(isA(Person.class));
    }
}

Mock工具之Mockito实战

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

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

这里使用了两个参数匹配器:

  isA():Object argument that implements the given class.

  eq():int argument that is equal to the given value

注:Mockito使用verify去校验方法是否被调用,然后使用isA和eq这些内置的参数匹配器可以更加灵活,

关于参数匹配器的详细使用请参考官网文档:https://static.javadoc.io/org.mockito/mockito-core/2.25.0/org/mockito/ArgumentMatchers.html

由于官网的代码和解释非常详细,此处就不再赘述。

仍然调用Junit执行单元测试代码,结果如图所示:

Mock工具之Mockito实战

验证了两种情况:

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

这里也可以查看Eclipse抛出的异常信息:

Argument(s) are different! Wanted:

personDao.getPerson(1);

-> at PersonServiceTest.testUpdateNotFind(PersonServiceTest.java:41)

Actual invocation has different arguments:

personDao.getPerson(2);

-> at PersonService.update(PersonService.java:8)

2 单元测试与覆盖率

1、Junit 2、JaCoCo 3、EclEmma

2 覆盖率

覆盖率如下图显示:

Mock工具之Mockito实战

覆盖率仍然使用JaCoCo和EclEmma:

l  未覆盖代码标记为红色

l  已覆盖代码会标记为绿色

l  部分覆盖的代码标记为黄色

颜色也可以在Eclipse中自定义设置:

Mock工具之Mockito实战

Mock工具之Mockito实战

Mock工具之Mockito实战

在Eclipse下方的状态栏窗口,有一栏“Coverage”,点击可以显示详细的代码覆盖率:Mock工具之Mockito实战

如何导出为Html格式的Report:

在Eclipse下方的Coverage栏鼠标右键选择“Export Session…”,在弹出窗口中选择export的目标为“Coverage Report”如下图:

Mock工具之Mockito实战

点击“Next”按钮后,在接下来的弹出窗口选择需要导出的session,Format

类型选择“HTML report”,导出位置暂时选择为桌面,都选择之后点击“Finish”按钮就生成好了。

Mock工具之Mockito实战

在桌面上找到一个叫做index.html的页面就是刚刚生成好的Coverage Report:

点击文件夹可以进入目录,进一步查看子文件的覆盖率:

Mock工具之Mockito实战

Mock工具之Mockito实战

附录:参考文档一览

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

5分钟了解Mockito:http://liuzhijun.iteye.com/blog/1512780

Mockito简单介绍及示例:http://blog.csdn.net/huoshuxiao/article/details/6107835

Mockito浅谈:http://www.jianshu.com/p/77db26b4fb54

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

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

JUnit+Mockito 单元测试(二):http://blog.csdn.net/zhangxin09/article/details/42422643

感谢阅读!作者原创技术文章,转载请注明出处

 看完点个赞呗,难道想白嫖不成?更多内容请访问微信公众号 :三国测,扫码关注哟!

其他推荐相关阅读:

单元测试系列之一:如何使用JUnit、JaCoCo和EclEmma提高单元测试覆盖率

测试系列之二:Mock工具Jmockit实战

单元测试系列之三:JUnit单元测试规范

单元测试系列之四:Sonar平台中项目主要指标以及代码坏味道详解

单元测试系列之五:Mock工具之Mockito实战

单元测试系列之六:JUnit5 技术前瞻

单元测试系列之七:Sonar 数据库表关系整理一(rule相关)

单元测试系列之八:Sonar 数据库表关系整理一(续)

单元测试系列之九:Sonar 常用代码规则整理(一)

单元测试系列之十:Sonar 常用代码规则整理(二)

单元测试系列之十一:Jmockit之mock特性详解

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这