使用Mockito进行单元测试

1、单元测试介绍

Mockito和Junit是用于单元测试的常用框架。单元测试即:从最小的可测试单元(如函数、方法或类)开始,确保每个单元都能按预期工作。单元测试是白盒测试的核心部分,它有助于发现单元内部的错误。

单元测试是目前常用的白盒测试方法之一。

2、白盒测试介绍

白盒测试,也称为结构测试、逻辑驱动测试或基于代码的测试,是一种针对被测单元内部工作原理进行测试的方法。它要求测试者完全了解被测软件的结构和内部工作原理,通过程序内部的代码和结构信息来设计测试用例,以确保软件的内部质量,减少软件的缺陷和漏洞,提高软件的可靠性和稳定性。

白盒测试总体上可以分为静态分析和动态分析两大类,具体包括以下几种方法:

2.1、静态分析

静态分析是在不执行程序的情况下进行的测试,主要关注代码本身的质量。

  1. 代码审查:对代码进行人工审查,以发现潜在的bug、漏洞或不符合编码规范的地方。结对编程就是属于这种。
  2. 代码扫描:使用自动化工具对代码进行扫描,以发现潜在的代码质量问题。SonarLint和SonarQube就是流行的代码自动化扫描工具。

2.2、动态分析

动态分析需要执行程序,并观察程序的运行行为和输出结果。

  1. 单元测试:从最小的可测试单元(如函数、方法或类)开始测试,确保每个单元都能按预期工作。
  2. 覆盖测试:包括语句覆盖、判定覆盖、条件覆盖、判定/条件覆盖、条件组合覆盖和路径覆盖等多种逻辑覆盖方法。这些方法旨在通过设计测试用例来覆盖程序中的所有逻辑路径和条件,以发现潜在的错误。

覆盖测试通常包括:

  • 语句覆盖:确保程序中的每个可执行语句至少被执行一次。
  • 判定覆盖:确保程序中的每个判定(分支)的每个分支至少被执行一次。
  • 条件覆盖:确保判定中的每个条件至少取到一次真值和一次假值。
  • 判断/条件覆盖:同时满足判定覆盖和条件覆盖的要求。
  • 条件组合覆盖:确保判定中所有条件的每一种组合至少出现一次。
  • 路径覆盖:确保程序中每一条可能的路径至少被执行一次,是最强的覆盖准则。

2.3、其他白盒测试

  1. 测试驱动开发(TDD):在某些情况下,白盒测试可能会与测试驱动开发结合使用,这意味着在编写实际代码之前首先编写测试用例。
  2. 持续集成:将白盒测试集成到持续集成流程中,确保每次代码更新后都能够自动运行测试,及时发现问题。gitlabCICD通常会集成test阶段,每次代码更新后会自动去跑代码中的测试用例。如果测试用例跑失败,则代码不会更新。

从白盒测试的介绍可以看出,白盒测试可以检测代码中的每条分支和路径,揭示隐藏在代码中的错误,对代码的测试比较彻底,有助于软件最优化。但相应的要想进行充分的白盒测试需要投入大量的时间人力,投入成本高昂,覆盖所有代码路径难度大,不能替代集成测试,不适用于快节奏的敏捷开发。

3、Mockito的使用

代码可以参考这个:【免费】一个Mockito的Demo资源-CSDN文库

引入mockito依赖

pom.xml

XML 复制代码
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-inline</artifactId>
   <version>4.3.1</version>
   <scope>test</scope>
</dependency>
复制代码
PeopleInfoServiceImpl.java
java 复制代码
package com.mockitoTest.service.impl;

import com.mockitoTest.entity.PeopleInfoDto;
import com.mockitoTest.mapper.PeopleInfoMapper;
import com.mockitoTest.service.PeopleInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.validation.ValidationException;
import java.util.List;

@Service
public class PeopleInfoServiceImpl implements PeopleInfoService {
    @Autowired
    private PeopleInfoMapper peopleInfoMapper;

    @Override
    public String registerPerson(PeopleInfoDto peopleInfoDto) {
        if (peopleInfoDto.getPeopleId() == null) {
//            throw new ValidationException("PeopleId不能为null");
            return "PeopleId不能为null";
        } else {
            List<String> peopleIdList = peopleInfoMapper.listAllPeopleId();
            if (peopleIdList.contains(peopleInfoDto.getPeopleId())) {
//                throw new ValidationException("PeopleId已经存在了");
                return "PeopleId已经存在了";
            }
        }
        if (peopleInfoDto.getIdCardNo() != null) {
            String regex = "(^\\d{15}$)|(^\\d{17}([0-9]|X)$)";
            if (!peopleInfoDto.getIdCardNo().matches(regex)) {
//                throw new ValidationException("身份证号不合法");
                return "身份证号不合法";
            }
        } else {
            return "身份证号不能为null";
        }
        if (peopleInfoDto.getPhone() != null) {
            String regex = "^(13|14|15|17|18)\\d{9}$";
            if (!peopleInfoDto.getPhone().matches(regex)) {
//                throw new ValidationException("手机号不合法");
                return "手机号不合法";
            }
        } else {
            return "手机号不能为null";
        }
        if (peopleInfoDto.getEmail() != null) {
            String regex = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$";
            if (!peopleInfoDto.getEmail().matches(regex)) {
//                throw new ValidationException("邮箱不合法");
                return "邮箱不合法";
            }
        } else {
            return "邮箱不能为null";
        }
        if (peopleInfoDto.getPwd() == null) {
//            throw new ValidationException("密码不能为null");
            return "密码不能为null";
        }
        //System.out.println(peopleInfoMapper.addPeopleInfo(peopleInfoDto));
        if (peopleInfoMapper.addPeopleInfo(peopleInfoDto) == 1) {
            return "注册成功";
        }
        return "未知错误";
    }
}

PeopleInfoServiceImpl.registerPerson()方法的测试方法(覆盖了所有分支)

java 复制代码
package com.mockitoTest;

import com.mockitoTest.entity.PeopleInfoDto;
import com.mockitoTest.mapper.PeopleInfoMapper;
import com.mockitoTest.service.PeopleInfoService;
import com.mockitoTest.service.impl.PeopleInfoServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;


import java.util.Arrays;

/**
 * @Author: Wulc
 * @CreateTime: 2024-09-15
 * @Description: 单元测试
 * @Version: 1.0
 */
class PeopleInfoServiceMockitoTest {
    //Mockito测试类不能是pubilc
    //InjectMocks会调用实际的方法(InjectMocks只能修饰具体的class,不能修饰接口)
    @InjectMocks
    private PeopleInfoServiceImpl peopleInfoService;

    @Mock
    private PeopleInfoServiceImpl peopleInfoServiceMock;

    //Mock不会调用实际方法,因此需设定其返回值。Mockito.when(调用的方法).thenReturn(你给定的返回值)
    @Mock
    private PeopleInfoMapper peopleInfoMapperMock;

    @Autowired
    private PeopleInfoService peopleInfoServiceAutowired;

    @BeforeEach
    void initBean() {
        //让注解生效
        MockitoAnnotations.initMocks(this);
    }

    @Test
    void registerPerson() {
        PeopleInfoDto peopleInfoDto = new PeopleInfoDto("qianqi", "钱七", "男", "420606198510233062", "18006588532", "18006588532@163.com", "www!@qw123456");
        PeopleInfoDto peopleInfoDto1 = new PeopleInfoDto("error", "钱七", "男", "420606198510233062", "18006588532", "18006588532@163.com", "www!@qw123456");
        //因为不想涉及实际数据库,所以所有PeopleInfoMapper类的方法,都自定一个调用此方法的返回值。
        //这样在测试过程中如果有调用到PeopleInfoMapper类中的方法,就不会调用实际方法了,而是调用一个模拟方法,不会操作数据库。
        Mockito.when(peopleInfoMapperMock.listAllPeopleId()).thenReturn(Arrays.asList("zhangsan12", "wangwu34"));
        Mockito.when(peopleInfoMapperMock.addPeopleInfo(peopleInfoDto)).thenReturn(1);
        Mockito.when(peopleInfoMapperMock.addPeopleInfo(peopleInfoDto1)).thenReturn(0);

        //校验peopleId为null
        PeopleInfoDto p0 = new PeopleInfoDto(null, "张三", "男", "420606198510233062", "15580703373", "15580703373@163.com", "www!@qw123456");
        Assertions.assertEquals("PeopleId不能为null", peopleInfoService.registerPerson(p0));

        //校验peopleId是否已存在
        PeopleInfoDto p1 = new PeopleInfoDto("zhangsan12", "张三", "男", "420606198510233062", "15580703373", "15580703373@163.com", "www!@qw123456");
        Assertions.assertEquals("PeopleId已经存在了", peopleInfoService.registerPerson(p1));

        //校验身份证号是否合法
        PeopleInfoDto p2 = new PeopleInfoDto("zhaoliu", "赵六", "男", "310107sasa196901033214", "13822297249", "13822297249@163.com", "www!@qw123456");
        Assertions.assertEquals("身份证号不合法", peopleInfoService.registerPerson(p2));

        //校验身份证号不能为null
        p2.setIdCardNo(null);
        Assertions.assertEquals("身份证号不能为null", peopleInfoService.registerPerson(p2));

        //校验手机号是否合法
        PeopleInfoDto p3 = new PeopleInfoDto("zhaoliu", "赵六", "男", "420606198510233062", "138222rr97249", "13822297249@163.com", "www!@qw123456");
        Assertions.assertEquals("手机号不合法", peopleInfoService.registerPerson(p3));

        //校验手机号不能为null
        p3.setPhone(null);
        Assertions.assertEquals("手机号不能为null", peopleInfoService.registerPerson(p3));

        //校验邮箱是否合法
        PeopleInfoDto p4 = new PeopleInfoDto("zhaoliu", "赵六", "男", "420606198510233062", "13822297249", "13822297249@##16323.com", "www!@qw123456");
        Assertions.assertEquals("邮箱不合法", peopleInfoService.registerPerson(p4));

        //校验邮箱不能为null
        p4.setEmail(null);
        Assertions.assertEquals("邮箱不能为null", peopleInfoService.registerPerson(p4));

        //校验密码为null
        PeopleInfoDto p5 = new PeopleInfoDto("zhaoliu", "赵六", "男", "420606198510233062", "13822297249", "13822297249@163.com", null);
        Assertions.assertEquals("密码不能为null", peopleInfoService.registerPerson(p5));

        //全部条件通过
        Assertions.assertEquals("注册成功", peopleInfoService.registerPerson(peopleInfoDto));

        //未知错误
        Assertions.assertEquals("未知错误", peopleInfoService.registerPerson(peopleInfoDto1));
    }
}

注意:代码中涉及的身份证号码和手机号是我从网上找的:

2023最新游戏防沉迷实名认证有效身份证号大全 - 游戏攻略 - UU站长网 (uuzzw.com)

在线手机号码生成器 - 随机生成手机号码 (bmcx.com)

运行一下:

所有测试用例都通过了

如果有测试用例失败,则会通过断言assert报错,这样程序就不会继续执行下去。

Intel Idea支持生成代码覆盖率报告

4、总结

因为工作中有被强制要求写单元测试,并且还对覆盖率有要求,所以我就学了一下Mockito。提交的代码如果单元测试覆盖率不够,会被SonarQube退回来了。

最近在看我导师推荐的《领域驱动设计软件核心复杂性应对之道》,里面有这么一句话:"影片的剪辑人员专注于准确完成自己的工作。他担心其他看到这部电影的剪辑人员会给他挑错。在这个过程中,镜头的核心作用被忽略了",我觉得这句话很有道理。

哈哈,其实也就同行会挑你的错了。比如你代码写的好不好,设计是否合理。测试、产品、PM、用户根本不会关心。测试只关心你的代码能否顺利通过测试,产品只关心功能能否实现,PM只关心能否及时交付,用户只关心什么时候上线?好不好用?

所以敏捷开发大行其道是有原因的!!!

5、参考资料

http://gitlab.is.eccom.com.cn/erpdev/erp-microservice/eccom-ts/ts-presale/ts-presale-clue

Mockito单元测试&Mockito对Service层的测试案例_mockito注入service-CSDN博客

3.pom.xml文件 - maven中scope标签和optional标签详解_pom scope-CSDN博客

断言(编程术语)_百度百科

自从用了Mockito,单元测试全是绿的 | Java 单元测试框架 | 高效开发小技巧_哔哩哔哩_bilibili

【Mockito】单元测试如何提升代码覆盖率_哔哩哔哩_bilibili

https://zhuanlan.zhihu.com/p/478920970

文心一言 (baidu.com)

相关推荐
王解4 小时前
Jest项目实战(4):将工具库顺利迁移到GitHub的完整指南
单元测试·github
Devil枫15 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
小袁在上班21 小时前
Python 单元测试中的 Mocking 与 Stubbing:提高测试效率的关键技术
python·单元测试·log4j
测试19981 天前
外包干了2年,快要废了。。。
自动化测试·软件测试·python·面试·职场和发展·单元测试·压力测试
安冬的码畜日常1 天前
【The Art of Unit Testing 3_自学笔记06】3.4 + 3.5 单元测试核心技能之:函数式注入与模块化注入的解决方案简介
笔记·学习·单元测试·jest
王解1 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
程序员小雷2 天前
软件测试基础:单元测试与集成测试
python·功能测试·selenium·测试工具·单元测试·集成测试·压力测试
王解2 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
程序员雷叔2 天前
外包功能测试就干了4周,技术退步太明显了。。。。。
功能测试·测试工具·面试·职场和发展·单元测试·测试用例·postman
安冬的码畜日常2 天前
【玩转 Postman 接口测试与开发2_005】第六章:Postman 测试脚本的创建(上)
javascript·测试工具·单元测试·postman·bdd·chai