草庐IT

Mockito详细教程

小楼夜听雨QAQ 2023-04-12 原文

前言

单元测试(UT)
工作一段时间后,才真正意识到代码质量的重要性。虽然囫囵吞枣式地开发,表面上看来速度很快,但是给后续的维护与拓展制造了很多隐患。
作为一个想专业但还不专业的程序员,通过构建覆盖率比较高的单元测试用例,可以比较显著地提高代码质量。如后续需求变更、版本迭代时,重新跑一次单元测试即可校验自己的改动是否正确。

Mockito和单元测试有什么关系?
与集成测试将系统作为一个整体测试不同,单元测试更应该专注于某个类。所以当被测试类与外部类有依赖的时候,尤其是与数据库相关的这种费时且有状态的类,很难做单元测试。但好在可以通过“Mockito”这种仿真框架来模拟这些比较费时的类,从而专注于测试某个类内部的逻辑。

SpringBoot与Mockito

spring-boot-starter-test中已经加入了Mockito依赖,所以我们无需手动引入。
另外要注意一点,在SpringBoot环境下,我们可能会用@SpringBootTest注解。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
public @interface SpringBootTest {

如果用这个注解,跑单元测试的时候会加载SpringBoot的上下文,初始化Spring容器一次,显得格外的慢,这可能也是很多人放弃在Spring环境下使用单元测试的原因之一。
不过我们可以不用这个Spring环境,单元测试的目的应该是只测试这一个函数的逻辑正确性,某些容器中的相关依赖可以通过Mockito仿真。

所以我们可以直接拓展自MockitoExtendsion,这样跑测试就很快了。

@ExtendWith(MockitoExtension.class)
public class ListMockTest {
}

基本使用

mock与verify

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;

import static org.mockito.Mockito.*;


@ExtendWith(MockitoExtension.class)
public class ListMockTest {

    @Test
    public void mockList() {
        List mockedList  = mock(List.class);

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

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

mock(List.class)会返回一个List的仿真对象,可以理解为“假对象”,要与后面提到的spy 区分开。
通过Mockito的verify来验证是否调用过List的add方法。

stubbing(存根)

什么是存根

注意:mocking和stubbing背后的理论很庞大。这里的解释只是针对于这个框架而言,比较粗浅。
上面通过mock函数得到了一个代理对象,调用这个对象的函数时,如果有返回值,默认情况下返回值都是null,如果基本类型,默认值是0或者false。

  @Test
    public void mockList() {
        List mockedList  = mock(List.class);

        System.out.println(mockedList.get(0));
    }

控制台输出

null

当测试的单元依赖这个mock对象的返回值时,我们可以通过提前申明这个函数的返回值来测试各种各样的场景。
提前申明的这个过程被称为存根。

@ExtendWith(MockitoExtension.class)
public class ListMockTest {

    @Test
    public void mockList() {
        List mockedList  = mock(List.class);

        //调用get(0)时,返回first
        when(mockedList.get(0)).thenReturn("first");
        //调用get(1)时,直接抛出异常
        when(mockedList.get(1)).thenThrow(new RuntimeException());

        //返回first
        System.out.println(mockedList.get(0));
        //抛出异常
        System.out.println(mockedList.get(1));

        //没有存根,则会返回null
        System.out.println(mockedList.get(999));
    }
}

注意点

  • 存根时可以被覆盖的(即对一种情况多次存根的话,以最后一次为准),但是不鼓励这么做,可读性会变差。
  • 一旦存根后,这个函数会一直返回这个值,不管你调用多少次。

返回值为void

即使有些函数返回值为void,也可以使用存根。

//调用clear方法时,抛出异常
doThrow(new RuntimeException()).when(mockedList).clear();

mockedList.clear();

连续存根

多次调用,返回不同的值。

    @Test
    public void mockList() {
        List mockedList  = mock(List.class);
        when(mockedList.get(0)).thenReturn(0).thenReturn(1).thenReturn(2);

        System.out.println(mockedList.get(0));
        System.out.println(mockedList.get(0));
        System.out.println(mockedList.get(0));
    }

返回值:

0
1
2

也可以简化为下面的这种写法,效果一样。

        when(mockedList.get(0)).thenReturn(0, 1, 2);

设置回调函数

调用某个函数的时候,执行一个回调函数。

    @Test
    public void mockList() {
        List mockedList = mock(List.class);
        when(mockedList.get(anyInt())).thenAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                System.out.println("哈哈哈,被我逮到了吧");
                Object[] arguments = invocationOnMock.getArguments();
                System.out.println("参数为:" + Arrays.toString(arguments));
                Method method = invocationOnMock.getMethod();
                System.out.println("方法名为:" + method.getName());

                return "结果由我决定";
            }
        });

        System.out.println(mockedList.get(0));
    }

控制台打印:

哈哈哈,被我逮到了吧
参数为:[0]
方法名为:get
结果由我决定

存根函数家族

除了上面出现的doReturn、doThrow、doAnswer外,还有:
doNothing() 啥也不干
doCallRealMethod() 调用真正的方法(不代理)

参数匹配器

基本用法

看完上面的存根,可能会有一个疑问:如果我想监控这个对象有没有被调用get方法,具体参数是什么我并不关心,该咋办。
这个时候就用到了参数匹配器。

    @Test
    public void mockList() {
        List mockedList  = mock(List.class);

        when(mockedList.get(0)).thenReturn("first");

        //返回first
        System.out.println(mockedList.get(0));

        //验证是否调用过get函数。这里的anyInt()就是一个参数匹配器。
        verify(mockedList).get(anyInt());
    }

处理anyInt(),还有很多的参数匹配器,默认的放在ArgumentMatchers类中。当然,也可以根据需求自定义参数匹配器或者使用hamcrest匹配器。
当一个函数接收多个参数时,如果其中有一个用了参数匹配器,那其他的参数也必须用。

    class Student{
        public void sleep(int id, String studNo, String name) {

        }
    }

    @Test
    public void mockStudent() {
        Student student = mock(Student.class);

        student.sleep(1, "1", "admin");

        verify(student).sleep(anyInt(), anyString(), eq("admin"));
        verify(student).sleep(anyInt(), anyString(), eq("admin"));
    }

正确的用法是:

    @Test
    public void mockStudent() {
        Student student = mock(Student.class);

        student.sleep(1, "1", "admin");

        verify(student).sleep(anyInt(), anyString(), eq("admin"));
    }

ArgumentCaptor

当我们需要去验证函数外部的一些参数时,就需要用到这个。
以发送邮件为例
定义一个邮件类:

@Data
@NoArgsConstructor
public class Email {

    private String to;
    private String subject;
    private String body;
    private EmailStyle emailStyle;

    public Email(String to, String subject, String body) {
        this.to = to;
        this.subject = subject;
        this.body = body;
    }
}

邮件有以下两种样式

public enum EmailStyle {
    HTML,DOC;
}

邮件服务会调用邮件平台发送邮件

public class EmailService {

    private DeliveryPlatform deliveryPlatform;

    public EmailService(DeliveryPlatform deliveryPlatform) {
        this.deliveryPlatform = deliveryPlatform;
    }

    public void send(String to, String subject, String body, boolean html) {
        EmailStyle emailStyle = EmailStyle.DOC;
        if(html) {
            emailStyle = EmailStyle.HTML;
        }

        Email email = new Email(to, subject, body);
        email.setEmailStyle(emailStyle);
        deliveryPlatform.deliver(email);
    }
}

邮件平台代码如下:

public class DeliveryPlatform {

    public void deliver(Email email) {
        //do something
    }
}

现在我想验证一个问题,当我发送HTML邮件时,deliver这个函数收到的email到底是不是HTML类型的。
这种情况下,就可以通过ArgumentCaptor的方式来解决了。

@ExtendWith(MockitoExtension.class)
public class EmailServiceTest {

    @Mock
    private DeliveryPlatform deliveryPlatform;

    @InjectMocks
    private EmailService emailService;

    @Captor
    private ArgumentCaptor<Email> emailArgumentCaptor;


    @Test
    public void testHtmlEmail() {
        emailService.send("某人", "无题", "无内容", true);

        verify(deliveryPlatform).deliver(emailArgumentCaptor.capture());

        Email email = emailArgumentCaptor.getValue();
        Assertions.assertEquals(EmailStyle.HTML, email.getEmailStyle());
    }
}

验证函数被调用的次数

下面的这个测试将不会通过

    @Test
    public void mockList() {
        List mockedList  = mock(List.class);

        when(mockedList.get(0)).thenReturn("first");

        //返回first
        System.out.println(mockedList.get(0));
        System.out.println(mockedList.get(0));

        //验证是否被用过get
        verify(mockedList).get(anyInt());
    }

报错如下:

org.mockito.exceptions.verification.TooManyActualInvocations: 
list.get(<any integer>);
Wanted 1 time:
-> at com.dayrain.mockitodemo.test.ListMockTest.mockList(ListMockTest.java:43)
But was 2 times:
-> at com.dayrain.mockitodemo.test.ListMockTest.mockList(ListMockTest.java:39)
-> at com.dayrain.mockitodemo.test.ListMockTest.mockList(ListMockTest.java:40)

大概意思是,只希望这个函数被调用一次,但实际上被调用了两次。
可能有点懵,不过点进verify方法后就明白了,默认情况下只调用一次;

public static <T> T verify(T mock) {
    return MOCKITO_CORE.verify(mock, times(1));
}

所以在调用的verify方法的时候,指定下调用次数即可。

verify(mockedList, times(2)).get(anyInt());

甚至支持不指定固定次数

 //一次也不能调用,等于times(0)
 verify(mockedList, never()).add("never happened");

 //至多、至少
 verify(mockedList, atMostOnce()).add("once");
 verify(mockedList, atLeastOnce()).add("three times");
 verify(mockedList, atLeast(2)).add("three times");
 verify(mockedList, atMost(5)).add("three times");

创建mock对象的另一种方式:@Mock

上述方法都是通过mock方法来构建仿真对象的,其实更简单的方法是通过注解。

    @Mock
    private List mockedList;

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

Spy(间谍)

介绍

上面讲的一些操作都是和Mock出来的对象相关的。通过mock()或者@Mcok注解标注的对象,可以理解为“假对象”。
Spy是针对于“真实存在”的对象。
在重构已有的旧代码时,Spy会比较好用。

    @Test
    public void spyList() {
        //申请了一个真实的对象
        List list = new LinkedList();
        List spy = spy(list);

        //可以选择存根某些函数
        when(spy.size()).thenReturn(100);

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

        //打印第一个元素
        System.out.println(spy.get(0));

        //获取list的大小
        System.out.println(spy.size());

        //验证
        verify(spy).add("one");
        verify(spy).add("two");
    }

当使用spy的时候,有一个很容易掉进去的陷进。即spy监听的是真实的对象,在操作真实对象的时候可能会出现越界之类的问题。

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

        //报错 IndexOutOfBoundsException, 因为这个List还是empty
        when(spy.get(0)).thenReturn("foo");
        //通过
        doReturn("foo").when(spy).get(0);
    }

注解

和@Mock类似,还可以用@Spy注解。

BDD(行为驱动开发)

针对比较流行的行为驱动开发,Mockito也提供了对应的支持:
如org.mockito.BDDMockito类中的given//when//then
BDD本文就不做拓展了,后续有时间再做梳理。

超时验证

如果要验证执行是否超时,可以这么做:

verify(student, timeout(1).times(1)).sleep(anyInt(), anyString(), eq("admin"));

自动实例化 @InjectMocks

下面举一个比较常见的例子
已有用户类

@Data
public class UserInfo {
    private String name;
    private String password;

    public UserInfo(String name, String password) {
        this.name = name;
        this.password = password;
    }
}

有对应的服务以及数据存储接口

@Service
public class UserInfoService {

    @Autowired
    private UserInfoDao userInfoDao;

    public void printInfo() {
        UserInfo userInfo = userInfoDao.select();
        System.out.println(userInfo);
    }
}

public interface UserInfoDao {
    UserInfo select();
}

如果我要测试这个service,并且不想和数据库有交互,那么可以创建一个UserInfoDao mock对象。
被测试类标注为@InjectMocks时,会自动实例化,并且把@Mock或者@Spy标注过的依赖注入进去。

@ExtendWith(MockitoExtension.class)
public class UserInfoServiceTest {

    @InjectMocks
    private UserInfoService userInfoService;

    @Mock
    private UserInfoDao userInfoDao;

    @Test
    public void testPrint() {
        UserInfo userInfo = new UserInfo("admin", "123");

        when(userInfoDao.select()).thenReturn(userInfo);

        userInfoService.printInfo();
    }
}

运行结果为:

UserInfo(name=admin, password=123)

参考

本文大部分内容来自于官网,但不会完全照搬,只整理我认为可能用得到的地方。并且可能会用自己的语言重新组织一下,或者替换部分示例代码,望谅解。
官网地址:https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
参考博客1:https://semaphoreci.com/community/tutorials/stubbing-and-mocking-with-mockito-2-and-junit
参考博客2:https://www.baeldung.com/mockito-argumentcaptor
如果您对其他语言的模拟也比较感兴趣,例如python,可以学习下面的博客:
https://semaphoreci.com/community/tutorials/getting-started-with-mocking-in-python

如有错误,欢迎指正!

有关Mockito详细教程的更多相关文章

  1. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  2. 在VMware16虚拟机安装Ubuntu详细教程 - 2

    在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主

  3. hadoop安装之保姆级教程(二)之YARN的配置 - 2

    1.1.1 YARN的介绍 为克服Hadoop1.0中HDFS和MapReduce存在的各种问题⽽提出的,针对Hadoop1.0中的MapReduce在扩展性和多框架⽀持⽅⾯的不⾜,提出了全新的资源管理框架YARN. ApacheYARN(YetanotherResourceNegotiator的缩写)是Hadoop集群的资源管理系统,负责为计算程序提供服务器计算资源,相当于⼀个分布式的操作系统平台,⽽MapReduce等计算程序则相当于运⾏于操作系统之上的应⽤程序。 YARN被引⼊Hadoop2,最初是为了改善MapReduce的实现,但是因为具有⾜够的通⽤性,同样可以⽀持其他的分布式计算模

  4. ruby - 在 RUBY 上的 PADRINO 框架上使用 RSPEC 进行测试的教程 - 2

    我是Ruby新手,并被要求在我们的新项目中使用它。我们还被要求使用Padrino(Sinatra)作为后端/框架。我们被要求使用Rspec进行测试。我一直在寻找可以指导在Padrino上使用RspecforRuby的教程。我得到的主要是引用RoR。但是,我需要RubyonPadrino。请在任何入门/指南/引用/讨论等方面指导我。如有不妥之处请指正。可能是我没有针对我的问题搜索正确的词/短语组合。我正在使用Ruby1.9.3和Padrinov.0.10.6。注意:我还提到了SOquestion,但它没有帮助。 最佳答案 我没用过Pa

  5. 100个python算法超详细讲解:画直线 - 2

    1.问题描述使用Python的turtle(海龟绘图)模块提供的函数绘制直线。2.问题分析一幅复杂的图形通常都可以由点、直线、三角形、矩形、平行四边形、圆、椭圆和圆弧等基本图形组成。其中的三角形、矩形、平行四边形又可以由直线组成,而直线又是由两个点确定的。我们使用Python的turtle模块所提供的函数来绘制直线。在使用之前我们先介绍一下turtle模块的相关知识点。turtle模块提供面向对象和面向过程两种形式的海龟绘图基本组件。面向对象的接口类如下:1)TurtleScreen类:定义图形窗口作为绘图海龟的运动场。它的构造器需要一个tkinter.Canvas或ScrolledCanva

  6. 区块链入门教程(6)--WeBASE-Front节点前置服务安装 - 2

    文章目录1.任务背景2.任务目标3.相关知识点4.任务实操4.1安装配置JDK4.2启动FISCOBCOS4.3下载解压WeBASE-Front4.4拷贝sdk证书文件4.5启动节点4.6访问节点4.7检查运行状态5.任务总结1.任务背景FISCOBCOS其实是有控制台管理工具,用来对区块链系统进行各种管理操作。但是对于初学者来说,还是可视化界面更友好,本节就来介绍WeBASE管理平台,这是一款微众银行开源的自研区块链中间件平台,可以降低区块链使用的门槛,大幅提高区块链应用的开发效率。微众银行是腾讯牵头设立的民营银行,在国内民营银行里还是比较出名的。微众银行参与FISCOBCOS生态建设,一定

  7. H2数据库配置及相关使用方式一站式介绍(极为详细并整理官方文档) - 2

    目录H2数据库入门以及实际开发时的使用1.H2数据库的初识1.1H2数据库介绍1.2为什么要使用嵌入式数据库?1.3嵌入式数据库对比1.3.1性能对比1.4技术选型思考2.H2数据库实战2.1H2数据库下载搭建以及部署2.1.1H2数据库的下载2.1.2数据库启动2.1.2.1windows系统可以在bin目录下执行h2.bat2.1.2.2同理可以通过cmd直接使用命令进行启动:2.1.2.3启动后控制台页面:2.1.3spring整合H2数据库2.1.3.1引入依赖文件2.1.4数据库通过file模式实际保存数据的位置2.2H2数据库操作2.2.1Mysql兼容模式2.2.2Mysql模式

  8. ruby-on-rails - rails 教程 : Putting flash messages in partial yields error "undefined method ` each' for nil:NilClass"? - 2

    这个问题在这里已经有了答案:关闭10年前。PossibleDuplicate:FlashMessagesinPartials(Rails3)我正在做MichaelHartl的Railstutorial和listing7.26将flash消息添加到应用程序布局:...">...这很好用。但是,我试图通过在我的部分文件夹中创建一个_flash.html.erb来清理这段代码...">-->...并且比使用......在我的应用程序布局中,我的所有Rspec测试开始失败,每个测试都显示以下消息:Failure/Error:before{visitsignup_path}ActionView:

  9. ruby-on-rails - Ruby on Rails 教程 - 5.26 - Sublime Text "Unable to Save"新文件 "spec/support/utilities.rb" - 2

    我正在使用SublimeText2,同时遵循MichaelHartl的RubyonRails教程。可以在http://ruby.railstutorial.org/book/ruby-on-rails-tutorial找到我所指的教程的具体部分。(ctrl+F“list5.26”)。我能够创建规范/支持文件。但是,在尝试创建spec/support/utilities.rb文件时,我收到消息“无法保存~/rails_projects/sample_app/spec/support/utilities.rb”。有人知道为什么会这样吗?SublimeText论坛上有人似乎遇到了完全相同的问

  10. 华为ensp详细安装包、安装教程及所遇问题 - 2

    目录一、安装包链接二、安装详细步骤1.安装Wireshark和WinPcap2.安装OracleVMVirtualBox3.安装ensp三、安装后注册四、启动路由器出现40错误怎么解决一、安装包链接二、安装详细步骤链接:https://pan.baidu.com/s/1QbUUYMOMIV2oeIKHWP1SpA?pwd=xftx提取码:xftx1.安装Wireshark和WinPcap找到Wireshark安装包所在文件夹,双击它,按照以下步骤安装。2.安装OracleVMVirtualBox找到OracleVMVirtualBox安装包所在文件夹,双击它,按照以下步骤安装。注:可自定义安装

随机推荐