1、Mock的概念

1.1. Mock的知识体系

在这里插入图片描述

1.2 什么是Mock

1.2.1 Mock是什么?

Mock(模拟的)是一种隔离测试类功能的方法。例如:Mock测试不需要真实连接数据库,或读取配置文件,或连接服务器。Mock对象模拟真实服务,Mock对象会返回与传递给它的某个虚拟输入相对应的虚拟数据

1.2.2 为什么要使用Mock?

单元测试重点在于验证代码逻辑以及结果是否正确,但是在测试过程中经常会遇到如下痛点问题:

  • 接口之间的相互依赖
  • 第三方接口调用,例如连接数据库,连接https等

使用Mock框架可以模拟出外部依赖,只注重测试代码逻辑、验证代码结果,满足测试真实目的

1.2.3 Mock的使用流程
  • 创建 外部依赖 的Mock 对象, 然后将Mock 对象注入到 测试类 中;

  • 执行 测试代码;

  • 校验 测试代码 是否执行正确

1.2.4 Mock的优点
  • 不需要启动Spring容器

  • 不需要与数据库发生真实交互,不会导致脏数据产生、不会受到数据库真实数据的影响、不需要为了单元测试额外添加/修改/删除数据;

  • 启动速度快、执行速度快、开发简单且效率高

1.3 Mock所需要的Maven依赖

test表示仅作用于测试目录。默认作用于范围compile表示被依赖项目需要参与当前项目的编译,包含测试目录。
mvn -Dmaven.test.skip clean install方式可以不编译测试用例

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>5.8.2</scope>
</dependency>
 
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <scope>4.3.1</scope>
</dependency>

2、Mock的使用

2.1 Mock方法介绍

Mock方法来自 org.mockito.Mock,它表示可以Mock一个对象或者是接口。

public static <T> T mock(Class<T> classToMock)

对于以下简单方法的Mock实例

public class MockitoDemo {
    public int add (int a, int b){
        return a + b;
    }
}

在类名上,鼠标右键,Generate--> Test 勾上要测试的方法,就会创建一个测试类

在这里插入图片描述

    @Test
    void add() {
        //mock方法来自org.mockito.Mock,它表示可以mock一个对象或者接口
        //官方文档中public static <T> T mock(Class<T> classToMock),这个classToMock 是待 mock 对象的 class 类
        //1.mock 一个对象的class类
        Random random = Mockito.mock(Random.class,"test");
        //2.使用random的nextInt方法
        System.out.println(random.nextInt());
        //3.验证是校验待验证的对象是否发生过某些行为,Mockito 中验证的方法是: verify
        Mockito.verify(random).nextInt();
        //4.Verify 配合 times() 方法,可以校验某些操作发生的次数
        Mockito.verify(random, Mockito.times(1)).nextInt();
    }

2.2 Mock对象打桩和断言

打桩的意思就是给 Mock对象规定一行的行为,使其按照我们的要求来执行具体的操作

Random random = Mockito.mock(Random.class,"test");
//5.打桩 用when方法
Mockito.when(random.nextInt()).thenReturn(100);
  • 打桩之后会用断言来验证;打桩的是期望值

  • 断言使用到的类是Assertions.里面的两个参数,第一个是期望值,第二个是实际值

Random random = Mockito.mock(Random.class,“test”);
Assertions.assertEquals(100, random.nextInt());

当使用Mock对象时,如果不对其行为进行定义,则Mock对象方法的返回值为返回类型的默认值

2.3 @Mock注解

想当于是快速的创建mock,模拟方法。不再用
Random random = Mockito.mock(Random.class,“test”);

而是直接在方法上面:

@Mock
private Random random; 

@Mock注解使用的注意事项

Mock 注解需要搭配MockitoAnnotarions.openMocks(testClass)方法一起使用

    @Mock
    private Random random;
 
    @Test
    void add() {
        //使@Mock注解生效
        MockitoAnnotations.openMocks(this);
        //打桩
        Mockito.when(random.nextInt()).thenReturn(100);
        //断言
        Assertions.assertEquals(100,random.nextInt());
    }

2.4 @BeforeEach和@AfterEach注解作用

@BeforeEach :用在测试前准备,测试前会构建很多的环境配置或者基础配置,都可以在这里设置

@AfterEach :用于测试后设置

  • 代码示例如下
 @Mock
    private Random random;
 
    @BeforeEach
    void setUp() {
        System.out.println("测试前的准备工作...");
        //使@Mock注解生效
        MockitoAnnotations.openMocks(this);
    }

    @AfterEach
    void after() {
        System.out.println("测试结束");
    }

    @Test
    void add() {
        //打桩
        Mockito.when(random.nextInt()).thenReturn(100);
        //断言
        Assertions.assertEquals(100,random.nextInt());
    }

在这里插入图片描述

2.5 @Spy注解和@Mock的区别

  • 被Spy的对象会走真实的方法,而Mock对象不会

  • Spy方法的参数是对象实例,Mock的参数是class

打桩测试 - 设置方法返回4,而实际期待值为3 ,测试不通过

在这里插入图片描述

不打桩测试 - 因为是Spy注解方式,会走真实的方法,测试通过

在这里插入图片描述

下面是案例代码,感兴趣可以自己执行操作试试

package org.example;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;


class MockitoDemoTest {

    @Spy
    private MockitoDemo mockitoDemo;

    @BeforeEach
    void setUp() {
        System.out.println("测试前的准备工作...");
        //使@Mock注解生效
        MockitoAnnotations.openMocks(this);
    }

    @AfterEach
    void after() {
        System.out.println("测试结束");
    }

    @Test
    void add() {
        // Mock打桩操作就是无论真实的方法如何执行,我们可以自行假设该方法的执行的结果
//        Mockito.when(mockitoDemo.add(1, 2)).thenReturn(4);
        // 使用断言测试结果准确性
//        Assertions.assertEquals(3, mockitoDemo.add(1, 2));
        // 调用MockitoDemo类真实的方法
        Mockito.when(mockitoDemo.add(1, 1)).thenCallRealMethod();
        Assertions.assertEquals(2, mockitoDemo.add(1, 1));
    }
}

2.6 Mock测试静态方法

  • 之前的版本中是不允许模拟测试静态方法的,如果需要测试静态方法,需要替换新的mock依赖,注释掉目前有的依赖,会有冲突

  • 静态方法测试要用Mock的MockedStatic类构建测试方法

        <!--        <dependency>-->
        <!--            <groupId>org.mockito</groupId>-->
        <!--            <artifactId>mockito-core</artifactId>-->
        <!--            <scope>4.3.1</scope>-->
        <!--        </dependency>-->

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>4.3.1</version>
            <scope>test</scope>
        </dependency>
  • 第一个静态方法打桩测试
    在这里插入图片描述

  • 第二个静态方法打桩测试

在这里插入图片描述

  • 单个的方法执行是没有问题的,但是我们在类上全部执行的时候,发现报错
    ,提示:static mocking is already registered in the current thread To create a new mock, the existing static mock registration must be deregistered

在这里插入图片描述

原因:意思就是说,每个方法需要有自己的static mock 对象,不允许公用。一起执行的时候,第一个方法占了对象,第二个方法就没有办法再占了

解决方法:每个方法执行完毕后就直接关闭mock对象 demo.close()。相当于是单例的。用完后就的释放,下一个方法才能接着使用

在这里插入图片描述

下面是案例代码,感兴趣可以自己执行操作试试

package org.example;

import lombok.Data;

import java.util.Arrays;
import java.util.List;

public class StaticUtils {

    public static String name;

    // 使用Mock测试方法
    public static List<Integer> range(int a, int b) {
        return Arrays.asList(a * b);
    }

    public static String getName() {
        return name;
    }

    public static void setName(String name) {
        StaticUtils.name = name;
    }
}
package org.example;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import java.util.Arrays;

public class StaticUtilsTest {

    @BeforeEach
    void setUp() {
    }

    // 第一个静态方法测试
    @Test
    void range() {
        MockedStatic demo = Mockito.mockStatic(StaticUtils.class);
        //打桩
        demo.when(()->StaticUtils.range(2,6)).thenReturn(Arrays.asList(10,11,12));
        Assertions.assertTrue(StaticUtils.range(2,6).contains(11));
        // 关闭Mock
        demo.close();
    }

    // 第二个静态方法测试
    @Test
    void name() {
        MockedStatic demo = Mockito.mockStatic(StaticUtils.class);
        //打桩
        demo.when(StaticUtils::getName).thenReturn("dhmw");
        Assertions.assertEquals("dhmw",StaticUtils.getName());
        // 关闭Mock
        demo.close();
    }
}

2.7 Mock提升测试覆盖率

案例:数据统计系统,地推人员输入客户的姓名和手机号码,最后构建用户对象存入数据表

业务代码如下:

  • User实体类
package dto;

import lombok.Data;

@Data
public class User {

    private String name;

    private String phone;
}

  • UserDao类
package dao;

import dto.User;


public class UserDao {

    public User save(String name, String phone) {
        User user = new User();
        user.setName(name);
        user.setPhone(phone);
        return user;
    }
}
  • RegistrationService接口
package services;

import dto.User;

import javax.xml.bind.ValidationException;


public interface RegistrationService {

    User register(String name, String phone) throws Exception;

}
  • RegistrationService接口实现类
package services;

import dao.UserDao;
import dto.User;
import org.springframework.beans.factory.annotation.Autowired;

import javax.xml.bind.ValidationException;

public class RegistrationServiceImpl implements RegistrationService {

    @Autowired
    UserDao userDao;

    @Override
    public User register(String name, String phone) throws Exception {
        if (name == null || name.length() == 0) {
            throw new ValidationException("name 不能为空");
        }
        if (phone == null || phone.length() == 0) {
            throw new ValidationException("phone 不能为空");
        }
        User user;
        try {
            user = userDao.save(name, phone);
        } catch (Exception e) {
            throw new Exception("SqlException thrown" + e.getMessage());
        }
        return user;
    }
}

生成相应的测试代码,此时有几个问题需要思考

  • 因为需要测试覆盖度,需要考虑该测试类中总共有几种情况需要测试

    • (1):两个if抛出了两个异常,总共是2种情况要测试

    • (2):保存数据库分为正常保存和异常保存,总共2种情况测试

综上所述,必须完成这四种情况的测试,才能覆盖所有代码

相同的方法,生成测试用例,代码中有详细的说明,每一种情况都有测试用例

package com.echo.mockito.service.impl;
 
import com.echo.mockito.dao.UserDao;
import com.echo.mockito.vo.User;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;
 
import javax.xml.bind.ValidationException;
 
import java.sql.SQLException;
 
import static org.junit.jupiter.api.Assertions.*;
 
class RegistrationServiceImplTest {
 
  @InjectMocks     //RegistrationServiceImpl 实例中注入@Mock标记的类,此处是注入userDao
    @Spy
    private RegistrationServiceImpl registrationService;
    @Mock
    private  UserDao userDao;
    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }
 
    @Test
    void register() throws Exception {
        // ------------------  第一种 name  异常情况   测试   start ------------------------
        String name = null;
        String phone = "1234";
        try {
          registrationService.register(name,phone);
        }catch (Exception e){
        Assertions.assertTrue(e instanceof ValidationException);
        }
 
        // ------------------  name  异常情况   测试   end  ------------------------
 
        // ------------------  第二种 phone  异常情况   测试   start  ------------------------
        name = "111";
        phone = null;
      try {
        registrationService.register(name,phone);
      }catch (Exception e){
        Assertions.assertTrue(e instanceof ValidationException);
      }
        // ------------------  phone  异常情况   测试   start  ------------------------
 
 
        // ------------------  第三种 userDao.save   正常情况   测试   start  ------------------------
      name = "111";
      phone = "111";
        //正常保存测试  打桩  走真实的方法
        Mockito.when(userDao.save(name,phone)).thenCallRealMethod();
        User user =  registrationService.register(name,phone);
        Assertions.assertEquals("111",user.getName());
        // ------------------  userDao.save   正常情况   测试   end  ------------------------
 
        // ------------------   第四种 userDao.save   异常情况   测试   start  ------------------------
      //异常保存测试  打桩   通过thenThrow 抛出异常  测试异常是否被捕获
      Mockito.when(userDao.save(name,phone)).thenThrow(new RuntimeException());
      try {
         registrationService.register(name,phone);
      }catch (Exception e){
        Assertions.assertTrue(e instanceof Exception);
      }
// ------------------  userDao.save   异常情况   测试   end  ------------------------
    }
}

更多推荐