概述


本文讲解的主要是有效和单元的思想,并不是说如何编写单元测试,用于改善和提高开发效率、编码风格、编码可读性和单测效率,不盲目追求覆盖率。

背景


  • 现在很多单元测试只是利用@Test注解把代码或者整个请求接口内的business做测试

  • 单测的过程就很多查数据库的方法,但是没必要每次都测sql,因为sql测一遍都应该是正确的。

  • 未明确单元测试由开发负责。单元测试是用于维护代码逻辑不被修改或者,修改了也不出错,不是测试的事情。

  • 单测代码启动速度、效率太低

  • 没有在各个环境整个工程单元测试通过

  • 方法写的很大,行数很多,边update边做逻辑

  • 很多公司盲目追求覆盖率

目的

  • 单元测试启动效率提升
  • 脱离环境,在每个环境都放心执行,不要考虑测试环境、生产环境有没有这条数据
  • 维护方法核心逻辑在后续的迭代中不被错误地修改
  • 用于改善和提高开发效率、编码质量、编码可读性、减少冗长的代码行

点赞再看,关注公众号:【地藏思维】给大家分享互联网场景设计与架构设计方案

掘金:地藏Kelvin https://juejin.im/user/5d67da8d6fb9a06aff5e85f7

什么是有效的单元测试


主要关键字: 有效、单元

  • 有效
  • 单元
  • 覆盖场景
  • 可重复执行
  • 断言
  • 不追求覆盖率

1.有效的定义

  • 只测试核心的业务逻辑,如计算逻辑类(指标计算)、用户支付下单组装数据库对象和子表(检验金额)、创建采买计划表和detail表的对象(校验指标个数、金额)
  • 不测用户交互的,如导出数据excel的底色、查询数据库记录
  • 不测逻辑的前置校验
  • 不要测试明显有用的东西。避免测试来自第三方供应商的类,特别是提供编写代码的框架的核心API的类。例如,不要测试向供应商的Hashtable类添加项、redis锁等第三方库
  • 不测环境相关的,尽量脱离环境,能让单测代码在每个环境都能正确执行。

2.单元的定义

  • “单元测试”中的“单元”的意思是,将每个单元设为原子和尽可能独立。
  • 目的:方法小,则可能组件化地再次利用,维护逻辑在以后的迭代中不被错误的修改。若被错误修改,只要每次迭代都走全工程的单测时将会报错而被发现。
  • 使得编写代码时方法职责单一,方法功能要小,方法逻辑小。也不要写多行数方法,以多个小方法组成一个长逻辑。
  • 单测时不要写大测试
  • 逻辑代码方法尽量不互相依赖。没有关于测试执行顺序的假设。
  • 逻辑代码方法适当的小,则能容易安装/拆卸
  • 单测时没必要把全流程都测一遍,也就是不必从controller遇到的第一个business入口测。只测核心逻辑,也就是核心的每个小方法的测试。

样例:

    public void updateUser(String userName,Integer age) {
// 校验
if (StringUtils.isEmpty(userName)) {
throw new RuntimeException();
}
// redis分布式锁
getRedis();
// 查询主表
selectUser();
// 查询出采买计划Detail
selectUserDetail();
// 对数据进行处理
calculateUserDetailInfo();
// 更新主表
updatePuchasePlanDb();
// 更新detail表
updatePuchasePlanDbDetail();
}

假设上述updateUser是一个Controller下的第一个business逻辑入口,用于更新用户信息,很多人的单元测试就会从updateUser这个方法开始做单元测试。但是这样就不符合有效、单元的理念。

无意义测试

  • 因为如redis这些第三方库,我们是相信他是正确的,而且走redis是需要连接真实网络,所以就会依赖环境,万一网络不通,单元测试这段代码就无法通过。
  • 前置校验也不用测,因为错误率和以后改动的机会比较少
  • 查询数据库主表、detail表、更新也不用测试,因为都是SQL,单元测试是为了维护逻辑不变,这些sql写好一遍能正确,以后的迭代中都是正确的。

有意义测试

  • 所以我们单测的时候只测数据处理的逻辑,假设数据处理逻辑如下,则对下面的3个方法每个做一个单元测试。
  • 我们会认为组件式的方法能正确,组合起来就大概率正确。因为方法足够的单元,则逻辑可插拔。
  • 若从头测到尾,以后迭代中逻辑改动的概率很大,断言错误概率大,这样单元测试维护的意义就很小。
    private void calculateUserDetailInfo() {
// 更改公司与关联上下级关系
changeCompany();
// 更改组与关联上下级关系
changeGroup();
// 更改部门与关联上下级关系
changeDeptment(); }

错误编码案例:

private void calculateBrand() {
for (int i = 0; i < new ArrayList<>().size(); i++) { for (int i1 = 0; i1 < new ArrayList<>().size(); i1++) {
if (new Integer(1)==1) { } else { }
}
} }

这样的代码就不符合单元的概念,至少把第二个for循环写在另外一个方法里,因为单元测试中,认为测试循环中一次是正确的,就断言循环中每次都大概率正确。

3.覆盖场景

  • 除了测试正常流程,还要测异常流程
  • 覆盖方法正常运行,为其创建一个单测
  • 覆盖if else,为同一个方法创建第二个单测

案例一 异常流程

    public Integer logic2(Integer num) throws Exception {
try {
num = this.purchasePlanDetailBusinessImpl.method(num);
} catch (RuntimeException e) {
throw new Exception();
}
return ++num;
}

则对应编写:

    /**
* 测试异常场景
*/
@Test(expected = Exception.class)
public void testMethodOnException() {
Integer method = this.purchasePlanBusinessImpl.logic(null);
}

4.断言

很多人的单元测试都是最后print一下,在console里面看日志或者debug看看是否正确,但是这样不足够。

  • 应有对方法返回值、对象里面的成员属性做判断
  • 判断是否为空或者判断金额数值或者判断异常是否符合预期
  • 只测试一件事一次。只要1个断言测试一或者多个特性/行为

如下,而不是直接判断是否为空或输出到终端

    @Test
public void testMethodOnNormal() {
Integer method = this.purchasePlanBusinessImpl.method(1); Assert.assertTrue(method == 2);
}

5.可重复执行

  • 目前很多操作数据库,或者内存数据库
  • 操作真实数据库,有可能下次就不能被断言成功了,因为数据有可能被update
  • 内存数据库虽然可以下次启动依然恢复默认的数据,但是有可能被其他人的单测操作过,导致自己的数据被错误修改

6.不盲目追求覆盖率

  • 很多公司盲目追求覆盖率,说美国google覆盖多高,但是别人工资高,6点准时下班,有足够时间开发,开发前uml设计好每个方法的入参出参,在中国是没有这么多时间,都是追求快速
  • 在中国大厂中,虽然有覆盖率要求,但是覆盖率只能证明你有没有写单元测试,但是单元测试写的好不好,就是另外一回事。因为有些人为了提高覆盖率,把一些无关的代码也去单元测试,如测试controller层、测试entity构造函数、utils工具类等。
  • 蚂蚁金服作为样例,蚂蚁是根据项目分覆盖级别,也不是所有的项目都要覆盖率多高,一般50%就很高了,不必都过50%,本文主要推崇的是有效、单元。
  • 按照本文的有效、单元的做法,是会牺牲覆盖率的,因为我们只测核心逻辑
  • 不浪费时间,因为中国互联网的迭代时间非常短,所以不必盲目为覆盖率而写单元测试,单元测试的目的是提供代码质量

操作工具


  • mockito:一般都能适用
  • powermock:在mockito的基础上,能测试private方法,还能mock static静态方法
  • @InjectMocks 用于框架new出对象,不用手动new 主要测试类
  • @Mock:被设置假对象返回的调用类
  • Assert.assertTrue等断言
  • JSONObejct、JSONString:有时自己mock对象需要自己new ,但是这个时候我们可以写好json体转换成bean会比较方便。
    <dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.7.RELEASE</version><!--$NO-MVN-MAN-VER$ -->
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.7.12</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.7.RELEASE</version><!--$NO-MVN-MAN-VER$ -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.7.RELEASE</version><!--$NO-MVN-MAN-VER$ -->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
</dependencies>
@RunWith(MockitoJUnitRunner.class)
public class PurchasePlanBusinessImplTest { @InjectMocks
private DoSomeBusinessImpl purchasePlanBusinessImpl;
@Mock
private DoSomeDetailBusinessImpl purchasePlanDetailBusinessImpl; /**
* 正常流程
*/
@Test
public void testMethodOnNormal() {
Integer method = this.purchasePlanBusinessImpl.method(1); Assert.assertTrue(method == 2);
} /**
* 在某些场景
*/
@Test
public void testMethodOnXX() {
when(this.purchasePlanDetailBusinessImpl.method(anyInt())).thenReturn(1);
Integer method = this.purchasePlanBusinessImpl.logic(1); Assert.assertTrue(method == 3);
}

影响力


效率提升

  • 目前测试要么启动服务、要么单测启动spring,其实都需要时间,无论是10几秒还是几分钟都是比较久的
  • 如果用mock方法,都是不需要基于sring容器,不需要自动注入就不需要解析bean关系,也不需要连接zk等环境问题。启动时间只需要1秒
  • 不需要在真实数据库造数据、不需要在内存数据库写sql
  • 如果测试整个工程的所有单测时,每一个类单元测试都会加载一次spring、内存数据库,导致跑整个工程都很久(实际上测试环境、回归环境都需要跑单测)

脱离环境

  • 其实每个环境,无论开发联调、测试、回顾、甚至生产环境都在构建时执行单元测试
  • 但是因为使用spring容器启动的方式,每个类的单测都需要启动spring,导致执行时间过长
  • 也或者因为环境出现造数据问题,导致执行不成功
  • mock出来的数据都是在代码实现,运行于内存中,所以不依赖中间件

方法小、行数少、职责小

  • 职责小:因单元测试的规则,让写每个方法时,都有意识地写的少依赖,这样的方法就提示被其他逻辑复用的概览,而不是大方法,导致要用差不多的逻辑时,其他同事就会去复制一份代码。(例如不会在一个方法又做查询数据库、校验、计算)
  • 提高方法被其他逻辑利用的概率,因为大方法很难重复利用

规范

  • 单元测试类以被测类的实现类为基础加Test,如xxxBusinessImpTest
  • 单元测试类需要放在test目录下,并且包名与被测类的路径一致,防止idea、sonar跑覆盖率校验时,没覆盖到对应的方法。
  • 方法命名:test开头,加真正的方法名,加场景,如testMethodOnNormal、testMethodOnException、testMethodOnLackOfMoney
  • 方法一般情况下都带返回值,即使没有返回值也写boolean。
  • 若是流程性方法等,可以为void,如责任链设计模式时
  • 必须断言,而不是print。断言值、正确与否、预期异常
  • 尽可能把查数据的逻辑写在被测方法之前,被测方法只做业务逻辑处理,不做查数据,这样单测时需要mock的方法会比较少。
  • 公司技术体系不一定会预留单元测试时间,很少公司愿意花时间在质量和维护上。都是赶着完成任务。
  • 例如3天开发,1天单元测试,1天sonar+review,1天联调,不一定有时间做得完善。

基于mockito做有效的单元测试的更多相关文章

  1. 基于Springboot+Junit+Mockito做单元测试

    前言 前面的两篇文章讨论过< 为什么要写单元测试,何时写,写多细 >和<单元测试规范>,这篇文章介绍如何使用Springboot+Junit+Mockito做单元测试,案例选取 ...

  2. Nunit工具做C#的单元测试

      Nunit工具做C#的单元测试 学习心得 编写人:罗旭成 时间:2013年9月2日星期一 1.开发人员如何做单元测试 单元测试是针对最小的可测试软件元素(单元)的,它所测试的内容包括单元的内部结构 ...

  3. 每日一帖示例程序(使用TWebBrowser基于HTML做)

    最近在程序中增加了每日一帖的功能,搜索一下网站的程序,发现大部分是用Memo实现,而我用的是TWebBrowser基于HTML做,故帖出来共享一下. PAS源码: unit Unit1; interf ...

  4. Haproxy基于ACL做访问控制

    author:JevonWei 版权声明:原创作品 haproxy配置文档 https://cbonte.github.io/haproxy-dconv/ 基于ACL做访问控制(四层代理) 网络拓扑 ...

  5. 基于OpenCV做“三维重建”(1)--找到并绘制棋盘

    <OpenCV计算机视觉编程攻略(第3版)>这套书已经出到第3版了,如果你非要我说这本书有多好,我说不出来:只是很多我第一手的例子都是来源于这本书的-相比较OpenCV官方提供的代码,这本 ...

  6. 基于Visual Studio .NET2015的单元测试

    基于Visual Studio .NET2015的单元测试 1.    在Visual Studio .NET2015中创建任意项目. 2.    在某个公共类的公共方法的名称上面点击右键,选择“创建 ...

  7. 基于 Redis 做分布式锁

    基于 REDIS 的 SETNX().EXPIRE() 方法做分布式锁 setnx() setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value) ...

  8. 基于Visual Studio .NET2015的单元测试 OpenCover

    https://www.cnblogs.com/XiaoRuLiang/p/10095723.html 基于Visual Studio .NET2015的单元测试 1.    在Visual Stud ...

  9. 基于 K8s 做应用发布的工具那么多, 阿里为啥选择灰姑娘般的 Tekton ?

    作者 | 邓洪超,阿里云容器平台工程师, Kubernetes Operator 第二人,云原生应用标准交付与管理领域知名技术专家   导读:近年来,越来越多专门给 Kubernetes 做应用发布的 ...

随机推荐

  1. 初识ABP vNext(6):vue+ABP实现国际化

    Tips:本篇已加入系列文章阅读目录,可点击查看更多相关文章. 目录 前言 开始 语言选项 语言切换 注意 最后 前言 上一篇介绍了ABP扩展实体,并且在前端部分新增了身份认证管理和租户管理的菜单,在 ...

  2. CSS 选择器及优先级

    CSS 选择器及优先级 1.根据权值计算 div .class1 #people的权值等于1+10+100=111 .class2 li #age的权值等于10+1+100=111 2.权值相同,那么 ...

  3. seo增加外链的方法

    http://www.wocaoseo.com/thread-128-1-1.html 今天给大家介绍一下本人发外链的一点经验吧.好多新手都感觉,发个外链真的好难哦.其实之前我也是这样认为的,发外链好 ...

  4. 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知

    乔丹是我听过的篮球之神,科比是我亲眼见过的篮球之神.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免 ...

  5. Codeforces 1321C Remove Adjacent

    题意 给你一个字符串,字符\(s_i\)可以被伤处当且仅当\(s_{i-1}=s_i-1\)或\(s_{i+1}=s_i-1\).问最多能删几个字符. 解题思路 其实,有个很简单的做法就是从\(z\) ...

  6. 大厂运维必备技能:PB级数据仓库性能调优

    摘要:众所周知,数据量大了之后,性能是大家关注的一点,所以我们在业务开发的时候,特别关注性能,做为一个架构师,必须对性能要了解,要懂.才能设计出高性能的业务系统. 一.GaussDB分布式架构 所谓集 ...

  7. php cookie及session

    1.会话控制概括 1)http协议的缺陷 无状态,就是无记忆,不能让同一浏览器和服务器进行多次数据交换时,产生业务的连续性, 2)什么是会话控制 会话控制就是解决http无记忆缺陷的,能够==将数据持 ...

  8. C:将算术表达式的符号和数分开

    程序: #include <stdio.h> #include <string.h> static int pos=; static char* line; void test ...

  9. Ajax跨域解决方案大全

    题纲 关于跨域,有N种类型,本文只专注于ajax请求跨域(,ajax跨域只是属于浏览器"同源策略"中的一部分,其它的还有Cookie跨域iframe跨域,LocalStorage跨 ...

  10. 白嫖党看番神器 AnimeSeacher

    - Anime Searcher - 简介 通过整合第三方网站的视频和弹幕资源, 给白嫖党提供最舒适的看番体验~ 目前有 4 个资源搜索引擎和 2 个弹幕搜索引擎, 资源丰富, 更新超快, 不用下载, ...