Mockito+junit5搞定单元测试

目录

  • 一、简介
    • [1.1 单元测试的特点](#1.1 单元测试的特点)
    • [1.2 Mock类框架的使用场景](#1.2 Mock类框架的使用场景)
    • [1.3 常见的Mock框架](#1.3 常见的Mock框架)
      • [1.3.1 Mockito](#1.3.1 Mockito)
      • [1.3.2 EasyMock](#1.3.2 EasyMock)
      • [1.3.3 PowerMock](#1.3.3 PowerMock)
      • [1.3.4 Testable](#1.3.4 Testable)
      • [1.3.5 比较](#1.3.5 比较)
  • 二、Mockito的使用
    • [2.1 导入pom文件](#2.1 导入pom文件)
    • [2.2 mock对象和spy对象](#2.2 mock对象和spy对象)
    • [2.3 初始化mock/spy对象的方式](#2.3 初始化mock/spy对象的方式)
    • [2.4 参数匹配](#2.4 参数匹配)
    • [2.5 方法插桩](#2.5 方法插桩)
    • [2.6 @InjectMocks注解的使用](#2.6 @InjectMocks注解的使用)
    • [2.7 断言工具](#2.7 断言工具)

一、简介

1.1 单元测试的特点

  • 配合断言使用(杜绝System.out)
  • 可重复执行
  • 不依赖环境
  • 不会对数据产生影响
  • Spring的上下文环境不是必须得
  • 一般都需要配合Mock类框架来实现的

1.2 Mock类框架的使用场景

要进行测试的方法存在外部依赖(如数据库,Redis,第三方接口调用等),为了能够专注对该方法或者单元的逻辑进行测试,就希望能够虚拟出外部依赖,避免外部依赖成为测试的阻塞项。

1.3 常见的Mock框架

Mock类框架:用于Mock外部依赖。

1.3.1 Mockito

官网:http://mockito.org/

官网文档:https://www.javadoc.io/doc/org.mockito/mockito-core/4.5.1/org/mockito/Mockito.html#13

限制:老版本对于final class、final method、statis method、private method均不能对Mockito mock,目前新版本已经支持final class、final method、statis method方法的mock,具体可以参考官网(有空了再补)

1.3.2 EasyMock

1.3.3 PowerMock

文档:https://github.com/powermock/powermock/wiki/Getting-Started

PowerMock是一款功能十分强大的Mock工具,其基本语法与Mockito兼容,同时扩展了许多Mockito缺失的功能,包括对支持对私有、静态和构造方法实施Mock。但由于使用了自定义类加载器,会导致Jacoco在默认的on-the-fly模式下覆盖率跌零。

powerMock是基于easyMock或Mockito扩展出来的增强版本,所以powerMock分两种类型,如果你习惯于使用easyMock的,那你就下载基于easyMock的powerMock,反之你喜欢用mockito的话就下载另一种PowerMock。

但是好像也没有多少人用。。。

1.3.4 Testable

文档:https://alibaba.github.io/testable-mock/#/

TestableMock现在已不仅是一款轻量易上手的单元测试Mock工具,更是以简化Java单元测试 为目标的综合辅助工具集,与PowerMock基本平齐,且极易上手,只需掌握一个@MockInvoke注解就可以完成绝大多数Mock操作。

1.3.5 比较

工具 原理 最小Mock单元 对被Mock方法的限制 上手难度 IDE支持
Mockito 动态代理 不能Mock私有方法 较容易 很好
PowerMock 自定义类加载器 任何方法皆可 较复杂 较好
JMockit 运行时字节码修改 不能Mock构造方法(new操作符) 较复杂 一般
TestableMock 运行时字节码修改 方法 任何方法皆可 较容易 较好

二、Mockito的使用

2.1 导入pom文件

导入Mockito坐标和junit5的坐标

xml 复制代码
	<dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.7.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>3.6.28</version>
      <scope>compile</scope>
    </dependency>

导入Mockito坐标和junit5的坐标,前期工作已经完成。

如果在springboot中我们还可以直接引用下面的坐标即可,其中的依赖已经包含了上述两者。

xml 复制代码
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>3.0.13</version>
    <scope>test</scope>
</dependency>

2.2 mock对象和spy对象

方法类型 方法插桩 方法不插桩 作用对象 最佳实践
mock对象 执行插桩逻辑 返回mock对象的默认值 类、接口 被测试类或者其他依赖
spy对象 执行插桩逻辑 调用真实方法 类、接口 被测试类
  • Mock不是真实的对象,它只是用类型的class创建了一个虚拟对象,并可以设置对象行为
  • Spy是一个真实的对象,但它可以设置对象行为

2.3 初始化mock/spy对象的方式

测试版本 方法一 方法二 方法三
junit4 @RunWith(MockitoJUnitRunner.class) + @Mock等注解 MockitoAnnotations.initMocks(this); Mockito.mock(x.class)
junit5 @ExtendWith(MockitoExtension.class)+ @Mock等注解 MockitoAnnotations.initMocks(this); Mockito.mock(x.class)

MockitoAnnotations.initMocks(this)方法已经被openMocks(this)替代。

我们现在来介绍一下初始化的三种方式:现阶段先关注初始化方法就行。。

java 复制代码
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

import org.junit.jupiter.api.BeforeEach;
import org.mockito.MockitoAnnotations;

import org.mockito.Mockito;

/**
 * 初始化mock/spy对象的第一种方式
 */
@ExtendWith(MockitoExtension.class)
public class TeacherServiceTestMethod1 {

    @Mock
    private TeacherService teacherService;
    @Spy
    private UserService userService;
    
    @Test
    public void test1(){
        //Mockito.mockingDetails(teacherService).isMock() 用来判断该对象是不是一个mock的对象
        System.out.println(Mockito.mockingDetails(teacherService).isMock());
        System.out.println(Mockito.mockingDetails(userService).isSpy());
        System.out.println();
    }
}

/**
 * 初始化mock/spy对象的第二种方式
 */
class TeacherServiceTestMethod2 {

    @Mock
    private TeacherService teacherService;
    @Spy
    private UserService userService;

    @BeforeEach
    void setUp() {
        //MockitoAnnotations.initMocks(this); 该方法已过时
        MockitoAnnotations.openMocks(this);
    }


    @Test
    public void test1(){
        System.out.println(Mockito.mockingDetails(teacherService).isMock());
        System.out.println(Mockito.mockingDetails(userService).isSpy());
        System.out.println();
    }
}

/**
 * 初始化mock/spy对象的第三种方式
 */
class TeacherServiceTestMethod3 {

    private TeacherService teacherService;
    private UserService userService;

    @BeforeEach
    void setUp() {
        userService = Mockito.mock(UserService.class);
        teacherService = Mockito.spy(TeacherService.class);
    }
    
    @Test
    public void test1(){
        System.out.println(Mockito.mockingDetails(teacherService).isMock());
        System.out.println(Mockito.mockingDetails(userService).isSpy());
        System.out.println();
    }
}

我们随意找一个Test方法debug一下只要看到对象是这样就ok了。注意$MockitoMock就是说明模拟成功了

com.surpass.service.UserService$MockitoMock$1070386111@55e8ec2f

2.4 参数匹配

将参数匹配和方法插桩一起示例。。。

2.5 方法插桩

指定调用某个方法时的行为(stubbing),达到相互隔离的目的。

  • 返回指定值
  • void返回值方法插桩
  • 插桩的两种方式
    • doXxx().when(obj).method(); 其中obj可以使mock/spy对象
    • when(obj.method()).thenXxx();其中obj可以使mock对象
  • 抛异常
  • 多次插桩
  • thenAnswer
  • 执行真正的原始方法
  • verify的使用
java 复制代码
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;

/**
 * 参数匹配:通过方法签名(参数)来指定哪些方法调用时需要被处理(插桩、verify验证)
 * 注意:在只用匹配器是要么都用要么都不用,禁止混搭!!举例
 * 正确:getUserListByTeacher("name", "address")或者getUserListByTeacher(anyString(), anyString())
 * 错误:getUserListByTeacher(anyString(), "address")
 */
@ExtendWith(MockitoExtension.class)
public class ParamMatcherTest {

    @Mock
    private TeacherService teacherService;

    /**
     * 对于mock对象不会调用真实方法,直接返回mock对象的默认值
     * 默认值(int)、null、空集合
     */
    @Test
    public void test1(){
        TeacherEntity teacher = teacherService.getTeacherByName("");
        System.out.println("teacher = " + teacher);
        Set<UserEntity> userListByTeacherName = teacherService.getUserListByTeacherName("");
        System.out.println("userListByTeacherName = " + userListByTeacherName);
    }

    /**
     * 方法插桩时的参数匹配
     * 参数匹配时类(ArgumentMatchers)是匹配参数的主要成员,例如:
     * any()则表示某一个方法传入任何类型都符合要求
     * anyString()则表示字符串类型都符合要求
     * !!!注意:所有的匹配都不包括null值
     */
    @Test
    public void test2(){
        TeacherEntity teacherEntity = new TeacherEntity();
        teacherEntity.setName("张三");
        teacherEntity.setAddress("北京");
        //when(teacherService.getTeacherByName("")).thenReturn(teacherEntity);  此行插桩方式也可,此插桩意为当执行getTeacherByName方法是会返回之前创建好的对象teacherEntity
        doReturn(teacherEntity).when(teacherService).getTeacherByName(any());

        TeacherEntity teacher = teacherService.getTeacherByName("123");
        System.out.println("teacher = " + teacher);
		//验证校验teacherService.getTeacherByName()调用的次数,之前调了一次所以校验通过,如两次则抛异常
        verify(teacherService, times(1)).getTeacherByName(any());
    }
}


控制台将打印如下内容
teacher = TeacherEntity(id=null, name=张三, address=北京, age=0)

其他项目的解释

java 复制代码
    @Mock
    private List<String> mockList;

    /**
     * 被插桩的方法在调用时不会执行实际的逻辑,直接返回指定的返回值
     */
    @Test
    public void test3(){
        /**
         * 指定返回值
         */
        //方法插桩:当调用mockList.get(0)时返回指定返回值"zero"
        doReturn("zero").when(mockList).get(0);
        Assertions.assertEquals("zero", mockList.get(0));

        when(mockList.get(1)).thenReturn("one");
        Assertions.assertEquals("one", mockList.get(1));

        /**
         * void返回值方法插桩
         */
        doNothing().when(mockList).clear();
        mockList.clear();
        verify(mockList, times(1)).clear();

        /**
         * 抛异常
         */
        doThrow(RuntimeException.class).when(mockList).clear();
        try {
            mockList.clear();
            //断言证明插桩失败
            Assertions.fail();
        } catch (Exception e) {
            Assertions.assertTrue(e instanceof RuntimeException);
        }
        //或
        when(mockList.get(anyInt())).thenThrow(RuntimeException.class);
        try {
            mockList.get(4);
            //断言证明插桩失败
            Assertions.fail();
        } catch (Exception e) {
            Assertions.assertTrue(e instanceof RuntimeException);
        }

        /**
         * 多次插桩
         * 意为第一次调用返回1,第二次调用返回2,第三次以及以后调用返回3
         */
        when(mockList.size()).thenReturn(1).thenReturn(2).thenReturn(3);
        //或-----两者相同
        when(mockList.size()).thenReturn(1, 2, 3);
        Assertions.assertEquals(1, mockList.size());
        Assertions.assertEquals(2, mockList.size());
        Assertions.assertEquals(3, mockList.size());
        Assertions.assertEquals(3, mockList.size());
    }

    /**
     * thenAnswer来实现对指定逻辑的插桩
     */
    @Test
    public void test4() {
        when(mockList.get(anyInt())).thenAnswer((Answer<String>) invocation -> {
            //getArgument表示获取插桩方法(此处为mockList.get(anyInt()))的第几个参数值
            Integer argument = invocation.getArgument(0, Integer.class);
            return String.valueOf(argument * 100);
        });
        //执行get方法
        System.out.println(mockList.get(1));
    }

	/**
     * 调用真实逻辑:控制台打印
     * 进入方法: getTeacher
	 * TeacherEntity(id=1, name=123, address=北京市, age=-1166257546)
     */
    @Test
    public void test5() {
        when(teacherService.getTeacher(any())).thenCallRealMethod();
        System.out.println(teacherService.getTeacher("123"));
    }
	

2.6 @InjectMocks注解的使用

  • 作用:若此注解声明的变量需要用的mock/spy对象,mockito会自动将当前类里面的mock/spy对象注入到当中
  • 原理:构造器注入、setter注入、字段反射注入

TeacherService实现 toString() 方法,调用 System.out.println(teacherService); 控制台打印如下内容:

shell 复制代码
TeacherService{teacherDao=teacherDao, userDao=userDao}

dubug能够看到两个Dao被自动注入到Service中去。

java 复制代码
@ExtendWith(MockitoExtension.class)
public class InjectMocksTest {

    /**
     * 被InjectMocks注解标注的助兴必须是实现类,因为mockito会创建对应的实例对象
     * 未经过mockito处理的普通对象会配合@spy注解使其变成默认调用真实方法的mock对象
     * mockito会使用spy对象或mock对象注入到InjectMocks对应的实例对象中
     */
    @Spy
    @InjectMocks
    private TeacherService teacherService;

    @Mock
    private TeacherDao teacherDao;
    @Mock
    private UserDao userDao;

    @Test
    public void test1(){
        System.out.println(teacherService);
    }
}

2.7 断言工具

  • hamcrest:junit4中引入的第三方断言库,junit5中被移除掉了。
  • assertj:常用断言库。
  • junit4原生断言库
  • junit5原生断言库

junit5常用断言类库:org.junit.jupiter.api.Assertions

相关推荐
爱上语文3 分钟前
Java LeetCode每日一题
java·开发语言·leetcode
bug菌26 分钟前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee
Манго нектар30 分钟前
JavaScript for循环语句
开发语言·前端·javascript
程序猿小D38 分钟前
第二百六十九节 JPA教程 - JPA查询OrderBy两个属性示例
java·开发语言·数据库·windows·jpa
阿华的代码王国1 小时前
【JavaEE】——文件IO的应用
开发语言·python
satan–01 小时前
R语言的下载、安装及环境配置(Rstudio&VSCode)
开发语言·windows·vscode·r语言
电饭叔1 小时前
《python语言程序设计》2018版第8章19题几何Rectangle2D类(下)-头疼的几何和数学
开发语言·python
Eternal-Student1 小时前
everyday_question dq20240731
开发语言·arm开发·php
极客先躯1 小时前
高级java每日一道面试题-2024年10月3日-分布式篇-分布式系统中的容错策略都有哪些?
java·分布式·版本控制·共识算法·超时重试·心跳检测·容错策略
卑微求AC2 小时前
(C语言贪吃蛇)11.贪吃蛇方向移动和刷新界面一起实现面临的问题
c语言·开发语言