如何写好单测

news2024/11/17 3:51:45

1、为什么要写单测?

单测即单元测试(Unit Test),是对软件的基本组成单元进行的测试,比如函数、过程或者类的方法。其意义是:

  • 功能自测,发现功能缺陷
  • 自我Code Review
  • 测试驱动开发
  • 促进代码重构并提升代码质量

1.1、代码覆盖率

单测质量最直接表现的指标就是代码覆盖率,分为语句覆盖(Statement coverage)、分支覆盖(Branch coverage)、条件覆盖(Condition converage)、路径覆盖(Path coverage)

1.2、单元测试 VS 集成测试

系统上线前都会做回归测试和集成测试,但为什么还要加单元测试呢?

指标对象单元测试集成测试
测试对象程序单元模块组合
测试方法白盒测试黑盒测试
测试时间开发阶段集成阶段
测试内容代码逻辑接口功能
测试粒度较细粒度较粗粒度

2、如何写好单测?

2.1、单测规约

可以参考阿里巴巴 的Java开发规范,以下几点在单测中要特别关注:

  • 【强制】好的单测必须遵守AIR原则。说明:单元测试在线上运行时,像空气一样感觉不到,但在测试的质量保证上,却是非常关键的。好的单元测试宏观上说,具体有自动化(Automatic)、独立性(Idependent)、可重复执行(Repeatable)的特点。
  • 【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.Out来进行人肉验证,必须使用Assert来验证
  • 【强制】单元测试是可以重复执行的,不能受到外界环境的影响。
  • 【推荐】编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量。
    • B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
    • C: Correct,正确的输入,并得到预期的结果。
    • D: Design,与设计文档相结合,来编写单元测试。
    • E: Error,强制错误信息输入(如:非法数据、 异常流程、业务允许外等),并得到预期的结果

2.2、一把好工具

写单侧首先要有好的单测工具,常用工具: Mockito、PowerMock、 EasyMock、JMockito等,Mock可以解决:

  • 解除对外部服务依赖
  • 减少全链路测试的数据准备
  • 模拟一些非正常的流程
  • 不用加载项目环境配置
  • 实现模块之间的并行开发

2.3、编写单元测试

在这里插入图片描述
可以把单元测试编写流程分为四大步骤,八大操作。

定义对象阶段

定义测试对象

在编写单元测试时,首先需要定义被测对象,或直接初始化、或通过Spy包装…实例化。

  • 直接构建对象
    UserService userService = new UserService();
  • 利用Mockito.spy方法
    UserService userService = Mockito.spy(new UserService());
    UserService userService = Mockito.spy(UserService.class);
  • 利用@Spy注解
@RunWith(PowerMockRunner.class) 
public class CompanyServiceTest {
	@Spy
	private UserService userService = new UserService();
}
  • 利用@InjectMocks注解
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
	@InjectMocks
	private UserService userService;
}

模拟依赖对象

在编写单元测试用例时,需要模拟各种依赖对象——类成员、方法参数和方法返回值。

  • 直接构建对象
UserDO user = new User(1L, “test”);
List<Long> userIdList = Arrays.asList(1L, 2L, 3L);
  • 反序列化对象
UserDO user = JSON.parseObject(text, UserDO.class);
List<UserDO> userList = JSON.parseArray(text, UserDO.class);
Map<Long, UserDO> userMap = JSON.parseObject(text, new TypeReference<Map<Long, UserDO>>() {});
  • 利用Mockito.mock方法
MockClass mockClass = Mockito.mock(MockClass.class);
List<Long> userIdList = (List<Long>)Mockito.mock(List.class);

  • 利用@Mock注解 @Mock
    private UserDAO userDAO;
  • 利用Mockito.spy方法
    UserService userService = Mockito.spy(new UserService());
  • 利用@Spy注解
    @Spy
    private UserService userService = new UserService(); // 必须初始化

注入依赖对象

在编写单元测试用例时,需要模拟各种依赖对象——类成员、方法参数和方法返回值。

  • 利用Setter方法注入
    userService.setMaxCount(100);
    userService.setUserDAO(userDAO);
  • 利用ReflectionTestUtils.setField方法注入
    ReflectionTestUtils.setField(userService, “maxCount”, 100);
    ReflectionTestUtils.setField(userService, “userDAO”, userDAO);
  • 利用Whitebox.setInternalState方法注入
    Whitebox.setInternalState(userService, “maxCount”, 100);
    Whitebox.setInternalState(userService, “userDAO”, userDAO);
  • 利用@InjectMocks注解注入
    @Mock
    private UserDAO userDAO; @InjectMocks
    private UserService userService;
  • 设置静态常量字段值
    FieldHelper.setStaticFinalField(UserService.class, “log”, log);

举个例子

@RunWith(PowerMockRunner.class)
public class UserSericeTest {
    // 模拟依赖对象(类成员)
	@Mock
	private UserDAO userDAO;
	
	// 定义测试对象
	@InjectMocks
	private UserService userService;
   
   @Before
   public void before() {
   		// 输入依赖对象(类成员)
		Whitebox.setInternalState(userService, "canModify", true);
   }
}

模拟方法阶段

在编写单元测试用例时,需要模拟方法指定参数并返回指定值。
在这里插入图片描述

举个例子

模拟依赖对象的数据可以自己构建、Mock或者可以从资源文件里读取。

@Test
public void testCreateUserWithCreate {
    // 模拟依赖对象方法:getIdByName
 	Mockito.doReturn(null).when(userDAO).getIdByName(Mockito.anyString());
	Long mockUserId = 2L;
	
	// 从资源文件加载
	String jsonData = ResourceHelper.getResouceAsString(getClass(), path + "/data.json")
	UserDO userDO = JSON.parseObject(jsonData, UserDO.class);
    // 
    Long userId = userService.createUser(userDO);
    Assert.assertEquals("用户标识不一致", mockUserId, userId);
    
	// 验证依赖方法
	Mockito.verify(userDAO).getIdByName(userDO.getUserName());
}

调用方法阶段

在这里插入图片描述

验证方法阶段

  • 验证依赖方法
    在这里插入图片描述
  • 验证数据对象
    在这里插入图片描述
  • 验证依赖对象
    在这里插入图片描述

3、 如何做的更好?

写代码不只是乱写一通,覆盖率上去了就可以了,它本质也是代码,也要符合代码规约。一个好的单测命名可以帮助理清单测Case 也可以便于他人Review。

3.1、规范命名

  • 测试类命名
    按照行业惯例,测试类的命名应以被测试类名开头并以Test结尾。 比如:UserServiceTest(用户服务测试类)
  • 测试方法命名
    按照行业规范,测试方法命名应以test开头并以被测试方法结尾。 a) 按照结果命名
    • testBatchCreateWithSuccess(测试:批量创建-成功)
    • testBatchCreateWithFailure(测试:批量创建-失败)
    • testBatchCreateWithException(测试:批量创建-异常)
    b) 按照参数命名
    • testBatchCreateWithListNull(测试:批量创建-列表为NULL)
    • testBatchCreateWithListEmpty(测试:批量创建-列表为空)
    • testBatchCreateWithListNotEmpty(测试:批量创建-列表不为空)
    c) 按照意图命名
    • testBatchCreateWithNormal(测试:批量创建-正常)
    • testBatchCreateWithGray(测试:批量创建-灰度)
    • testBatchCreateWithException(测试:批量创建-异常)
  • 测试资源命名-语义化 建议优先使用这些参数和变量的名称,并加后缀“.json”标识文件格式。 比如:userCreateList.json

3.2、各环节做好验证

  • 不验证返回值 不验证返回值,怎么能保证方法返回了正确值?
  • 不验证方法调用 不验证方法调用,怎么能保方法被正确的调用?
    Ø 不验证方法参数 不验证方法参数,怎么能保证传递数据的正确性?
    Ø 不验证异常信息 不验证异常信息,怎么能保证抛出异常的正确性?

4、常见单测问题

在编写单元测试用例时,或多或少会遇到一些问题,大多数是由于对测试框架特性不熟悉导致,比如:

  • Mockito不支持对静态方法、构造方法、final方法、私有方法的模拟,应该使用PowerMock功能;
  • Mockito的any相关的参数匹配方法并不支持可空参数和空参数,应该使用nullable方法;
  • 未Mock方法或Mock方法参数不匹配时,会返回默认值(基础类型为0,对象类型为null);
  • 采用Mockito的参数匹配方法时,其它参数不能直接用常量或变量,应该使用Mockito的eq方法;
  • 采用Argument的captor方法时,其它参数不能直接用常量或变量,应该使用Mockito的eq方法;
  • 使用when-then语句模拟Spy对象方法会先执行真实方法,应该使用do-when语句;
  • PowerMock对静态方法、构造方法、final方法、私有方法的模拟需要把对应的类添加到
    @PrepareForTest注解中;
  • PowerMock模拟JDK的静态方法、构造方法、final方法、私有方法时,需要把使用这些方法的类
    加入到@PrepareForTest注解中,但会导致单元测试覆盖率不被统计;
  • PowerMock使用自定义的类加载器来加载类,可能导致系统类加载器认为有类型转化问题;需要加上@PowerMockIgnore({“javax.crypto.*”})注解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/390656.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

File、递归、IO流(一)、IO流(二)

目录 ​File类概述 File类的常用API 判断文件类型、获取文件信息 创建文件、删除文件功能 遍历文件夹 方法递归 递归的形式和特点 递归的算法流程、核心要素 递归常见案例 递归的经典问题 非规律化递归案例-文件搜索 非规律化递归案例-啤酒问题 字符集 常见字符集…

美团二面经历——如何设计一个百万人抽奖系统?

文章目录 导图V0——单体架构V1——负载均衡V2——服务限流防止用户重复抽奖拦截无效流量服务降级和服务熔断V3 同步状态V4线程优化V5业务逻辑V6流量削峰通用思路单一职责URL动态加密静态资源——CDN服务限流数据预热削峰填谷导图 导图按照由浅入深的方式进行讲解,架构从来不是…

西电计算机组成原理(计组)核心考点汇总(期末真题+核心考点)

文章目录前言一、真题概览1.1 计组1历年真题1.2 计组2历年真题二、知识点说明2.1 计组12.1.1 冯诺依曼计算机组成和特点2.1.2 复杂指令系统计算机和特点2.1.3 精简指令系统计算机的特点2.1.4 指令长度的影响因素2.1.5 控制器2.1.6 微指令特性2.2 计组22.2.1 SMP特点与优点2.2.2…

QML动态对象管理

QML中有多种方式来动态创建和管理QML对象&#xff1a; Loader &#xff08;加载器&#xff09;Repeater&#xff08;复制器&#xff09;ListView&#xff0c;GridWiew&#xff0c;PethView&#xff08;视图&#xff09; &#xff08;之后会介绍&#xff09;使用加载器&#xff…

剖析G1 垃圾回收器

简单回顾 在Java当中&#xff0c;程序员在编写代码的时候只需要创建对象&#xff0c;从来不需要考虑将对象进行释放&#xff0c;这是因为Java中对象的垃圾回收全部由JVM替你完成了(所有的岁月静好都不过是有人替你负重前行)。 而JVM的垃圾回收由垃圾回收器来负责&#xff0c;在…

刷题记录:牛客NC200179Colorful Tree 奇奇怪怪的dfs序

传送门:牛客 题目描述: A tree structure with some colors associated with its vertices and a sequence of commands on it are given. A command is either an update operation or a query on the tree. Each of the update operations changes the color of a specifi…

论文阅读 - End-to-End Wireframe Parsing

文章目录1 概述2 L-CNN2.1 整体架构2.2 backbone2.3 juction proposal module2.4 line sample module2.5 line verificatoin module3 评价指标参考资料1 概述 本文是ICCV2019的一篇论文&#xff0c;核心是提出了一种简单的end-to-end的two-stage的检测图像中线段的方法。同时&…

192、【动态规划】leetcode ——64. 最小路径和:回溯法+动态规划(C++版本)

题目描述 原题链接&#xff1a;64. 最小路径和 解题思路 &#xff08;1&#xff09;回溯法 分别向右或下进行探查 class Solution { public:int res INT_MAX;void backtracking(vector<vector<int>>& grid, int x, int y, int pathSum) {// 超出边界&…

高可用 - 08 Keepalived集群中Master和Backup角色选举策略

文章目录概述实例说明“weight”值为正数“weight”值为负数总结概述 在Keepalived集群中&#xff0c;其实并没有严格意义上的主、备节点&#xff0c;虽然可以在Keepalived配置文件中设置“state”选项为“MASTER”状态&#xff0c;但是这并不意味着此节点一直就是Master角色。…

Python实现人脸识别,进行视频跟踪打码,羞羞的画面统统打上马赛克

哈喽兄弟们&#xff0c;我是轻松~ 今天我们来实现用Python自动对视频打马赛克前言准备工作代码实战效果展示最后前言 事情是这样的&#xff0c;昨天去表弟家&#xff0c;用了下他的电脑&#xff0c;不小心点到了他硬盘里隐藏的秘密&#xff0c;本来我只需要用几分钟电脑的&…

第一章初识Linux

文章目录Linux简介LInux的应用领域Linux OS和各种发行版的关系Linux和Unix的关系Linux相关环境配置图解VM和Linux的关系Linux自定义分三个区VMware网络连接的三种模式桥接模式NAT模式主机模式VMware快照功能Linux的操作方式Linux的目录结构各种Linux发行版本的常见目录注意事项…

GO进阶(5) 垃圾回收机制

一、前言 1、垃圾回收背景 编程语言通常会使用手动和自动两种方式管理内存&#xff0c;C、C 以及 Rust 等编程语言使用手动的方式管理内存&#xff0c;工程师需要主动申请或者释放内存&#xff1b;而 Python、Ruby、Java 和 Go 等语言使用自动的内存管理系统&#xff0c;一般都…

Java八股——wait、sleep与park

sleep()、wait()、park()都可以使线程进入等待状态&#xff0c;但是3种方式在使用上和功能上都有些不同。 共同点: wait()&#xff0c;wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权&#xff0c;进入阻塞状态它们都可以被打断唤醒都是native方法执行sleep…

java四种线程池(基本使用)

标题java四种线程池及使用示例 1、线程工厂 1、我们先来写ThreadFactory&#xff0c;在创建线程池时候可以传入自定义的线程工厂&#xff0c;线程工厂说白了就是用来定制线程的一些属性&#xff1a;名字、优先级、是否为守护线程。直接看代码即可。 当然创建线程池的时候可以…

控制台运行java

控制台执行java 新建java代码 新建一个记事本文件&#xff0c;将文件名改为HelloWorld.java&#xff0c;注意&#xff1a;后缀是.java。 若没有显示文件后缀&#xff0c;可以在资源管理器打开显示后缀&#xff0c;然后再次修改文件名&#xff0c;一定要修改成文件类型是java…

缺陷及缺陷管理

今日目标能够说出缺陷的判定标准能够说出描述缺陷的6大核心内容能够描述缺陷状态、严重程度、优先级的作用能够按照提供的缺陷模版完成一个缺陷的提交能够说出缺陷的跟踪流程能够在禅道中提交测试用例能够在禅道中提交缺陷1. 缺陷1.1 缺陷的定义&#xff08;重点&#xff09;产…

Python3.8.8-Django3.2-Redis-连接池-数据类型-字符串-list-hashmap-命令行操作

文章目录1.认识Redis1.1.优点1.2.缺点2.在Django中Redis的连接3.Redis的基础用法3.1.hashmap结构3.2.list结构4.命令行查看数据库5.作者答疑1.认识Redis Remote DIctionary Server(Redis) 是一个key-value 存储系统&#xff0c;是跨平台的非关系型数据库。是一个开源的使用 AN…

Linux入门篇-Linux目录结构

简介 简单介绍Linux目录。 Linux的⽬录结构 “/”表示根⽬录&#xff0c;根⽬录是Linux⽬录结构中的最顶级的⽬录&#xff0c;类似于windows的C:\ D:\ /boot⽬录&#xff1a;存放的是系统的启动配置⽂件和内核⽂件 /dev⽬录&#xff1a;存放的是Linux的设备⽂件 /etc⽬录&…

Nacos新手详细知识大全

官网&#xff1a;Spring Cloud Alibaba一、Nacos快速入门1.1 服务注册到Nacos1.1.1 配置**引入com.alibaba.cloud&#xff0c;**以后的版本就不用操心了<!-- com.alibaba.cloud--><!-- com.alibaba.cloud--><dependency><groupId>com.alibaba.cloud<…

SpringCloud之 LoadBalancer负载均衡

文章目录LoadBalancer 负载均衡一、LoadBalanced 负载均衡二、自定义负载均衡三、OpenFeign 实现负载均衡①添加依赖②启动类添加 EnableFeignClients③创建客户端接口 UserClient④service业务中调用客户端接口提示&#xff1a;以下是本篇文章正文内容&#xff0c;SpringCloud…