单元测试系统化讲解之Mockito

857

单元测试系统化讲解之Mockito

一、理论概述

1.1 测试的分类

总结来说,测试的种类主要有以下几类

  1. 单元测试:
    1. 完成最⼩的软件设计单元(模块)的验证⼯作。
    2. ⽬标是确保模块被正确的编码,使⽤过程设计描述作为指南,对重要的控制路径进⾏测试以发现模块内的错误。
    3. 通常情况下是⽩盒的,对代码风格和规则、程序设计和结构、业务逻辑等进⾏静态测试,及早的发现和解决不易显现的错误。
  2. 集成测试
    1. 通过测试发现与模块接⼝有关的问题。
    2. ⽬标是把通过了单元测试的模块拿来,构造⼀个在设计中所描述的程序结构,应当避免⼀次性的集成(除⾮软件规模很⼩),⽽采⽤增量集成。
    3. ⾃顶向下集成:模块集成的顺序是⾸先集成主模块,然后按照控制层次结构向下进⾏集成,⾪属于主模块的模块按照深度优先或⼴度优先的⽅式集成到整个结构中去。
    4. ⾃底向上集成:从原⼦模块开始来进⾏构造和测试,因为模块是⾃底向上集成的,进⾏时要求所有⾪属于某个给顶层次的模块总是存在的,也不再有使⽤稳定测试桩的必要。
  3. 系统测试
    1. 是基于系统整体需求说明书的⿊盒类测试,应覆盖系统所有联合的部件。
    2. 系统测试是针对整个产品系统进⾏的测试,⽬的是验证系统是否满⾜了需求规格的定义,找出与需求规格不相符合或与之⽭盾的地⽅。
    3. 系统测试的对象不仅仅包括需要测试的产品系统的软件,还要包含软件所依赖的硬件、外设甚⾄包括某些数据、某些⽀持软件及其接⼝等。
    4. 因此,必须将系统中的软件与各种依赖的资源结合起来,在系统实际运⾏环境下来进⾏测试。
  4. 回归测试
    1. 回归测试是指在发⽣修改之后重新测试先前的测试⽤例以保证修改的正确性。
    2. 理论上,软件产⽣新版本,都需要进⾏回归测试,验证以前发现和修复的错误是否在新软件版本上再次出现。
    3. 根据修复好了的缺陷再重新进⾏测试。回归测试的⽬的在于验证以前出现过但已经修复好的缺陷不再重新出现。
    4. ⼀般指对某已知修正的缺陷再次围绕它原来出现时的步骤重新测试。
  5. 验收测试
    1. 验收测试是指系统开发⽣命周期⽅法论的⼀个阶段,这时相关的⽤户或独⽴测试⼈员根据测试计划和结果对系统进⾏测试和接收。
    2. 它让系统⽤户决定是否接收系统。
    3. 它是⼀项确定产品是否能够满⾜合同或⽤户所规定需求的测试。验收测试包括Alpha测试和Beta测试。
      1. Alpha测试:是由⽤户在开发者的场所来进⾏的,在⼀个受控的环境中进⾏。
      2. Beta测试:由软件的最终⽤户在⼀个或多个⽤户场所来进⾏的,开发者通常不在现场,⽤户记录测试中遇到的问题并报告给开发者,开发者对系统进⾏最后的修改,并开始准备发布最终的软件。

1.2 Mock介绍

  • Mock是什么?

    • mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为。
    • 这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。
  • 使用Mock能够为我们带来哪些好处呢?

    1. 团队可以并行工作
      • 有了Mock,前后端人员只需要定义好接口文档就可以开始并行工作,互不影响,只在最后的联调阶段往来密切;
      • 后端与后端之间如果有接口耦合,也同样能被Mock解决;
      • 测试过程中如果遇到依赖接口没有准备好,同样可以借助Mock;
      • 不会出现一个团队等待另一个团队的情况。这样的话,开发自测阶段就可以及早开展,从而发现缺陷的时机也提前了,有利于整个产品质量以及进度的保证。
    2. 开启TDD模式,即测试驱动开发
      • 单元测试是TDD实现的基石,而TDD经常会碰到协同模块尚未开发完成的情况,但是有了mock,这些一切都不是问题。
      • 当接口定义好后,测试人员就可以创建一个Mock,把接口添加到自动化测试环境,提前创建测试。
    3. 可以模拟那些无法访问的资源
      • 比如说,你需要调用一个“墙”外的资源来方便自己调试,就可以自己Mock一个。
    4. 隔离系统
      • 假如我们需要调用一个post请求,为了获得某个响应,来看当前系统是否能正确处理返回的“响应”,但是这个post请求会造成数据库中数据的污染,那么就可以充分利用Mock,构造一个虚拟的post请求,我们给他指定返回就好了。
    5. 可以用来演示
      • 假如我们需要创建一个演示程序,并且做了简单的UI,那么在完全没有开发后端服务的情况下,也可以进行演示。
      • 说到演示了,假如你已经做好了一个系统,并且需要给客户进行演示,但是里面有些真实数据并不想让用户看到,那么同样,你可以用Mock接口把这些敏感信息接口全部替换。【博主亲身经历过 0.0】
    6. 测试覆盖度
      • 假如有一个接口,有100个不同类型的返回,我们需要测试它在不同返回下,系统是否能够正常响应,但是有些返回在正常情况下基本不会发生,难道你要千方百计地给系统做各种手脚让他返回以便测试吗?
      • 比如,我们需要测试在当接口发生500错误的时候,app是否崩溃,别告诉我你一定要给服务端代码做些手脚让他返回500 。。。而使用mock,这一切就都好办了,想要什么返回就模拟什么返回,妈妈再也不用担心我的测试覆盖度了!
  • 对于开发人员来说,使用Mock有哪些作用呢?

    • 解决依赖问题:当测试一个接口或者功能模块的时候,如果这个接口或者功能模块依赖其他接口或其他模块,那么如果所依赖的接口或功能模块未开发完毕,那么我们就可以使用Mock模拟被依赖接口,完成目标接口的测试;
    • 单元测试:如果某个功能未开发完成,又要进行测试用例的代码编写,也可以先模拟这个功能进行测试;
    • 模拟复杂业务的接口:实际工作中如果我们在测试一个接口功能的时候,如果这个接口依赖一个非常复杂的接口业务或者来源于第三方接口,那么我们完全可以使用Mock来模拟这个复杂的业务接口,其实这个和解决接口依赖是一样的原理;
    • 前后端联调:进行前后端分离编程时,如果进行一个前端页面开发,需要根据后台返回的状态展示不同的页面,那么就需要调用后台的接口,但是后台接口还未开发完成,完全可以借助mock来模拟后台这个接口返回想要的数据;
  • Mock与Stub(桩):

    • 概念:
      • Mock:是模拟的意思,指的是在测试包中创建一个结构体,满足某个外部依赖的接口interface{}。
      • stub:是桩的意思,指的是在测试包中创建一个模拟方法,用于替换生成代码中的方法。
    • 区别: 1.mock和stub都是采用替换的方式来实现,被测试的函数中的依赖关系,不过mock采用的是接口替换的方式,stub采用的是函数替代的方式。 2.mock对代码没有倾入性,sub倾入性比较强在实现功能函数的时候,就需要为测试设置一些回调函数,也就是这里所说的桩。 3.对于控制被替代的方法来讲,mock如果想支持不同的输出,就需要提前实现不同分支的代码,甚至需要定义不同的mock结构来实现,这样的mock代码会变成一个支持所有逻辑分支的一个最大集合,mock代码复杂性会变高;stub却能很好的控制桩函数的不同分支,因为stub替换的是函数,那么只要需要再用带这种输出的时候,定义一个函数即可,而这个函数甚至可以是匿名函数。

二、认识Mockito

2.1 Mockito简介

  • 概述:
    • 测试驱动的开发(Test Driven Design, TDD)要求我们先写单元测试,再写实现代码。在写单元测试的过程中,一个很普遍的问题是,要测试的类会有很多依赖,这些依赖的类/对象/资源又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。
    • 所幸,我们有一个应对这个问题的办法:Mock。简单地说就是对测试的类所依赖的其他类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,然后提供给被测试对象使用。被测试对象像使用真的对象一样使用它们。用这种方式,我们可以把测试的目标限定于被测试对象本身,就如同在被测试对象周围做了一个划断,形成了一个尽量小的被测试目标。
    • Mock的框架有很多,最为知名的一个是Mockito,这是一个开源项目,使用广泛。

    简而言之,Mockito是一款在java中比较出名且优秀的单元测试工具;

mockito特点

  1. 它既能mock接口也能mock实体类(咱测试框架mock工具也能做到)
  2. 简单的注解语法-@Mock
  3. 简单易懂,语法简单
  4. 支持顺序验证
  5. 客户化参数匹配器

Mockito是一个单元测试框架,封装了简洁易用的API,对于新手来说也可以很容易的入门,同时在我们编写单元测试的时候还可以以锻炼我们的逻辑思维能力。

1.3 使用Mockito需要注意的关键点有哪些?

  1. mock数据:在测试某个服务的时候,需要提前把这个依赖的接口mock 数据,一般都是使用注解:@mock 、 @InjectMocks
    1. @mock: 由Mockito框架根据接口的实现帮我们mock一个虚假对象,该对象的方法不会真正去执行
    2. @InjectMocks: 由Mockito框架根据接口的实现帮我们创建一个实例对象(注意是实例对象,所以调用该类的方法都会被执行),同时会根据@Mock的类与@InjectMocks标识的类中的属性类型进行匹配,如果完全一致,自动注入属性值,类似于Spring的IOC功能
    3. 参考代码如下:
      @Mock
      private ImageSercice imageSercice;
      @Mock
      private RiskSercice riskSercice;
      @InjectMocks
      private UserSercice userSercice;
      
  2. 设置预期:由于依赖其他小伙伴的接口,所以我们可以在调用接口的时候设置预期的参数以及返回值,在程序执行的时候mockito自动就会帮我们返回预计设置的值(注意这里不会真正调用依赖的接口)。当然这个只是其中一种设置预期,比如说我们还可以设置某个方法调用的次数等信息。
    1. Mockito.mock: 创建一个模拟类实例对象,与@mock的作用是一样的,注意不是@InjectMocks,想要注入依赖对象还是老老实实的使用@InjectMocks吧。当我们使用这个方法创建出来的对象,结合when().thenReturn()时候,调用的方法是不会执行的
    2. Mockito.when: 用于调用某个方法的时候设置期望,具体请看如下DEMO,设置了当调用imageService.upload(参数)时返回true,调用riskService.risk(参数)时返回new HashMap():
      1. 代码示例:
         @Test
        public void testWhen() {
            Mockito.when(imageService.upload(Mockito.any())).thenReturn(true);
            Mockito.when(riskService.risk(Mockito.anyMap())).thenReturn(new HashMap());
            userService.save(new HashMap<>());
        }
        
    3. Mockito.spy::与mock类似,但他创建的是实例对象,所以调用其方法会被真实执行的,结合when().thenReturn()时候看下图DEMO所示: 在这里插入图片描述
    4. Mockito.doThrow: 校验方法出现异常时的校验:
      // 保存用户的时候抛出Exception异常
      Mockito.doThrow(new NullPointerException()).when(Mockito.mock(UserService.class)).save(null);
      
    5. Mockito.eq: 一般在设置Mockito.when参数值时使用,判断参数等于预期值,如果不相等则设置的 when 无效,如果不写的话,默认就是eq;但是如果一个方法有多个参数且不同类型,做参数值判断的时候Mockito.eq 必须写,否则运行值会报错。
      Mockito.when(userService.getById(1)).thenReturn(userModel); 
      // 当单个参数是,可以改写成 
      Mockito.when(userService.getById(Mockito.eq(1))).thenReturn(userModel); 
      
      // 多个参数,校验的Mockito参数类型参数不同,此种情况每个参数都必须添加特定类型参数值校验,
      // 示例demo:userService.get(age,name,date)
      // 错误写法
      Mockito.when(userService.get(1,null,new Mockito.isA(Date.class))).thenReturn(userModel);
      // 正确写法
      Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.isA(Date.class))).thenReturn(userModel);
      // 也是正确写法,但是new Date 与程序中的new Date是两个对象,不会命中预设值的Mockito.when(userService.get(1,null,new Date())).thenReturn(userModel); 
      Mockito.when(userService.get(1,null,new Date())).thenReturn(userModel);
      
    6. Mockito.any: 返回一个对象,支持Null,一般是在mockito.when() 被调用的方法中使用,表示忽略对象值:
      // 与 Mockito.eq 中的demo类似 ,只是将Mockito.isA 换成了any
      Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.any(Date.class))).thenReturn(userModel);
      
    7. Mockito.isNull: 返回一个空对象,一般是在mockito.when() 被调用的方法中使用:
      // 与 Mockito.any 中的demo类似 
      Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.any(Date.class))).thenReturn(userModel);
      
    8. Mockito.isA: 返回一个指定类型对象与any 类似
      // 与 Mockito.any 中的demo类似 
      Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.any(Date.class))).thenReturn(userModel);
      
    9. Mockito.doNothing(): 只是声明一个Mock方法,基本上它告诉Mockito在调用模拟对象中的方法时什么也不做。有时用于void返回方法或没有副作用的方法,或者与正在进行的单元测试无关
  3. 验证结果:一般来说设置预期与设置验证结果都是一起的。如果在一个单元测试中,如果你设定的预期没有得到满足,那么这个单元测试就是失败的。例如设置预期结果是调用userService.save的时候用户名必须走风控,头像必须上传到第三方服务器上,但是在测试中它并没有被调用,那么测试就失败了。
    1. Mockito.verify: 校验方法,传入不同的校验规则做不同的校验,如下示例为校验某个方法被调用的次数校验:
      // 校验 riskService.risk 方法被调用一次
      Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));
      
    2. Mockito.times: 对调用次数的校验,不单独使用,需结合Mockito.verify() 方法:
      // 校验 riskService.risk 方法被调用一次:Mockito.times(次数)
      Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));
      
    3. Mockito.never: 表示一个方法从来没有调用过,与Mockito.times(0)一样
    4. Mockito.reset: 重置Mock的对象的相关联引用:
      userService.save(userModel);
      // 验证通过
      Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));
      // 重置已经统计的次数
      Mockito.reset(userService);
      userService.save(userModel);
      // 验证通过
      Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));
      
    5. Mockito.atLeastOnce: 表示某个方法至少调用 X 次
    6. Mockito.atMost: 表示某个方法最多调用 X 次
    7. Mockito.timeout: 调用某个方法超时时间
    8. Mockito.validateMockitoUsage:手动检查堆栈是否是空的;

总之,可以先记住我们的过程主要分为三个:Mock数据->设定预期->验证结果。要注意其中设定预期是可以被省略的。至于里面的方法,忘了可以再回来看或者翻官方文档;

三、Mockito应用

3.1 引入Mockito的简单Demo

  1. Maven引入Mockito :
    <dependency>
    	<groupId>org.mockito</groupId>
    	<artifactId>mockito-core</artifactId>
    	<version>3.8.0</version>
    	<scope>test</scope>
    </dependency>
    
  2. 上面依赖导入后,就支持Mockito了。接下来我们进行实战演示;先建一个Person对象类
    public class Person{
    	private int id;
    	private String name;
    	private String key;
    
    	public Person(int id,String name){
    		this.id = id;
    		this.name = name;
    		this.key = id + name + id;
    	}
    	
    	// setter、getter方法省略
    }
    
  3. 编写简单的单元测试类
    public class PersonTest{
    	@Test
    	void verifyTest(){
    		Person mockPerson = mock(Person.class);
    		mockPerson.setId(1);
    		mockPerson.setName("TestOps");
    
    		verify(mockPerson).setId(1);
    		verify(mockPerson).setName("TestOps");
    	}
    }
    

    验证了setId(1)和 setName("TestOps")这两个方法是否被调用了;

  4. 其他小知识:

3.2 测试桩Demo

  • 概述:Mockito可以做一些测试桩(Stub)
  • 代码示例
@Test
void subTest{
	Person mockPerson = mock(Person.class);
	when(mockPerson.getId()).thenReturn(1);
	when(mockPerson.getName()).thenThrow(new NoSuchMethodError());

	// 单元测试通过
	System.out.println(mockPerson.getId());
	// 单元测试抛出异常
	System.out.println(mockPerson.getName());
}

3.3 java风格验证参数值

  • 概述:
    • Mockito以自然的java风格来验证参数值
    • 为了合理的使用复杂的参数匹配,使用equals()与anyX()的匹配器会使得测试代码更简洁、简单。
  • 代码示例:
    • Person.Class 中增加setKyeById方法:
      public String setKeyById(int id){
      	this.key = id + this.name + id;
      	return this.key;
      }
      
    • 测试代码:
      @Test
      void matchersTest(){
      	Person mockPerson = mock(Person.class);
      	when(mockPerson.setKeyById(anyInt())).thenReturn("00testops00");
      	System.out.println(mockPerson.setKeyById(5));
      	verify(mockPerson).setKeyById(anyInt());
      }
      

3.4 验证函数的确切、最少、从未调用次数

  • 概述:
    • verify函数默认验证的是执行了times(1),也就是某个函数是否执行了1次,因此,times(1)通常被省略了;
  • 代码示例:
    @Test
    void timesTest(){
    	Person mockPerson = mock(Person.class);
    	mockPerson.setId(1);
    	verify(mockPerson).setId(1);
    	mockPerson.setName("testops");
    	mockPerson.setName("testops");
    	
    	verify(mockPerson,times(2)).setName("testops");
    	verify(mockPerson,atLeast(2)).setName("testops");
    	verify(mockPerson,never()).setId(2);
    }
    

3.5 使用Mockito验证执行顺序

  • 概述:
    • 验证执行顺序是非常灵活的,不需要一个一个的验证所有交互,只需要验证感兴趣的对象即可。
    • 另外,可以仅通过那些需要验证顺序的mock对象来创建InOrder对象;
  • 代码示例:
    @Test
    void orderTest(){
    	Person singleMock = mock(Person.class);
    	singleMock.setId(1);
    	singleMock.setId(2);
    	
    	InOrder inOrder = inOrder(singleMock);
    	inOrder.verify(singleMock).setId(1);
    	inOrder.verify(singleMock).setId(2);
    	
    	Person firstMock = mock(Person.class);
    	Person secondMock = mock(Person.class);
    	
    	firstMock.setId(1);
    	secondMock.setId(2);
    	
    	InOrder inOrderT = inOrder(firstMock,secondMock);
    	// 这里会抛出异常,因为执行setId(2)的方法在set(1)的方法之后执行的;
    	inOrder1.verify(secondMock).setId(2);
    	inOrder1.verify(firstMock).setId(1);
    }
    

3.6 连续测试桩(sub)

  • 概述:
    • 有时候我们需要为同一个函数调用的不同返回值或异常做测试桩;
    • 比如一个方法被多次调用,我们期望每次调用能够执行不同的返回结果等场景;
  • 代码示例:
    @Test
    void consecutiveStubTest(){
    	Person personMock = mock(Person.class);
    	when(personMock.getName())
    		.thenReturn("testops")
    		.thenThrow(new NoSuchElementException());
    	System.out.println(personMock.getName());	// testops
    	System.out.println(personMock.getName());	// 异常
    }
    

3.7 spy:监控真实对象

  • 概述:
    • 可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会被调用,除非它的函数被stub了。
    • 尽量少使用spy对象,使用时也需要小心。
    • 可以使用spy对象来处理遗留老代码。
  • 代码示例:
    @Test
    void spyTest(){
    	Person person = new Person(1,"testops");
    	Person spy = spy(person);
    	
    	when(spy.getName()).thenReturn("mango");
    	System.out.println(spy.getId());
    	System.out.println(spy.getName());
    	
    	// 使用spy对象时真实的对象也会被调用,除非它被Mock了;
    	// 所以,当真实对象不可被调用时,请使用doReturn | Answer |  Throw()
    	doReturn("1testops1").when(spy).setKeyById(-1);
    	System.out.println(spy.setKeyById(-1));
    }
    

四、Mockito在SpringBoot中的使用

4.1 概述

  1. 从Spring Boot项目结构上来说,Service层是依赖Dao层的,而Controller层又依赖于Service层。
  2. 从单元测试角度,对某个Service和Controller进行单元的时候,他所有依赖的类都应该进行Mock;而Dao层单元测试就比较简单了,只依赖数据库中的数据。

4.2 隔离代码示例

  • 下面我们将在对Service层进行测试时,使用Mockito来隔离Dao层;这样就不会影响到数据库了;
    1. 前置准备如下:
      1. Service代码如下: 在这里插入图片描述
      2. dao层代码如下: 在这里插入图片描述
      3. Token对象如下: 在这里插入图片描述
    2. 方式一:直接使用Mockito提供的mock方法即可以模拟出一个服务的实例。再结合when/thenReturn等语法完成方法的模拟实现; 在这里插入图片描述
    3. 方式二:使用@Mock注解来Mock对象时的第一种实现,即使用MockitoAnnotations.initMocks(testClass). 在这里插入图片描述
    4. 方式三:在测试用例上加上@RunWith(MockitoJUnitRunner.class)这个注解后,就可以自由的使用@Mock来Mock对象;注:JUnit4 在这里插入图片描述
    5. 方式四:还可以使用MockitoRule。这里需要注意的是如果使用MockitoRule的话,该对象的访问级别必须为public;注:JUnit4 在这里插入图片描述