单元测试,写起来到底有多痛?

 到底什么是单元测试

  这个问题看似非常简单,单元测试嘛,不就是咱们开发自己写些测试类,来测试自己写的代码逻辑对不对。

  这句话没有问题,但是不够准确。

  首先我们要明白,这个测试二字前面还有两个字: 单元 。

  它要求我们的测试粒度,小

  具体来说就是一个 Test 仅测试一个方法,对这句话的认识非常重要。

  市面上常见的错误单测是怎样的呢:

  把整个项目启动,开始玩真的调用,入参是数据库里面真的值,所有的操作都落库,一个 Test 从 controller 到 service 再到 dao,一条龙打通。

  这种不叫单元测试, 这叫集成测试 。

  如果你现在写的是这样的“单测”,你就会发现,写个测试类不仅要依赖数据库,还要依赖缓存,依赖公司别的团队的服务,亦或是一些三方开放平台的 Http 服务。

  当我们的测试类需要依赖太多太多外部因素的时候,只要有一个地方出现问题,你的测试就是 fail 的。

  并且入参和出参不能“任你摆布”,你还得想着如何控制别的团队的服务返回你想要的数据。

  比如我想测试当依赖的服务 A 返回 sucess 时,我的代码逻辑的正确性,还得想测试服务 A 返回 fail 的逻辑,还想测试它返回 null 的逻辑。

  再包括数据库或者缓存的一些返回值的定制,这非常的困难,已经开始劝退人了。

  然后 把整个项目启动 ,这通常需要花费数分钟甚至数十分钟的时间,写两个单测一下午过去了,时间都花在调试的启动上了。

  所以才会有那么多程序员觉得,单测好难写啊,又耗时,还动不动就 fail,写个 P。

  所以回过头来看,到底什么是单测?

  在 Java 中,单元测试的对象是类中的某个方法,一个 Test 只需要关心这个方法的逻辑正确性,仅仅测试这个方法的逻辑,不应该也不需要关注外部的逻辑。

  举个例子,当你写 service 的单测时候,你压根就不应该测试 dao 或者外部服务返回的对不对,这是属于它们的逻辑,跟我 service 没有关系。

  可能听着感觉不强烈,我拿代码举个例:

  假设我们要测试 trainingYes 这个方法,可以看到方法内部依赖 yesDao 和 OneOneZeroProvicer ,一个是数据库,一个是 RPC 服务。

  这时候我们的思维应该是:不管传入的 id 在数据库中对应的 yes 数据到底如何,我想让 yesDao 返回 null 的时候它就要返回 null ,想让它不为 null 就不为 null。

  对 OneOneZeroProvicer 也是一样,我想随意操控让它返回 false 或者 true。

  因为数据库和外部服务的逻辑跟我当前的这个 service 方法没关系, 我只需要拿到我应该拿到的值来测试我的方法内部的所有逻辑分支即可 。

  只有这样,我们才能容易的测试到我们所写的代码逻辑。

  你想想看,如果你要是测着 trainingYes 还得管着到底哪个 id 能拿到值啊,然后这个 yesDao#getYesById 内部逻辑有没有状态过滤啊,这个 id 对应的数据有被废弃吗,需要关心这个那个,这就非常累了。

  再或者你想关心 OneOneZeroProvicer#call 怎样才能返回 true,怎样才能返回 false,这就更难了,因为这是别的团队的服务,你连这个服务的代码权限都没,一个一个去问别人?

  万一没这样的数据呢,还得去造?

  总而言之,单元测试仅需要关注自己方法内部的逻辑,不需要关注依赖方。

  看到这,很多同学就搞不懂了,那该怎么搞?我的代码就是依赖它们的服务了啊。

  这就涉及到 mock 了。

  mock 指的是伪造一个假的依赖服务,替换真正的服务,在上面的例子中,需要伪造 yesDao 和 OneOneZeroProvicer ,我们操控它得到我们想要的返回值,满足我们自身对 trainingYes 的测试需求。

  我拿 yesDao 举例一下,如下所示,我 mock 了一个假的 dao:

  然后 在单测时通过反射或者 set 注入的方式把 MockYesDao 注入到测试的 YesService 中 , 这样一来,是不是就能控制逻辑了?

  当我传入的 id 是 1 的时候,百分百拿到一个不是 null 的 yes 对象,当传入其他值的时候,肯定拿到的是 null,这样就非常容易控制我要测试的逻辑。

  当然,上面仅仅只是举例说明 mock 的含义的具体作用方式,实际上真正单测的时候没有人会手动写 mock 服务,基本上用的都是 mock 框架。

  比如我用的就是 mockito,这个我们后面再提。

  至此,你应该对如何写单测有点感觉了,我简单总结下上面说的几个小点:

  1. 单测不应该启动整个项目(包括 Spring 容器),没有这个必要,耗时长。

  2. 单测不应该关心依赖的服务,包括 Dao、provider等其它服务,需要通过 mock 来解耦。

  3. 一个测试方法只测当前要测试的一个类中的一个方法。

  其实就是分而治之的思想,本身在写代码的时候你已经为了降低复杂度和解耦,把代码分成了一个一个模块,一个个方法,而单元测试的目的,本就是验证这些你拆分的方法自身逻辑的正确性。

  为什么单测这么难写

  在对单测有点感觉之后,我们再来盘一盘为什么单测这么难写。

  核心原因在于, 我们本身写的代码不够解耦 。

  看到这有人不服了,什么?单测难写还怪我本身写的代码不好,难写是因为本身的业务逻辑复杂!

  好吧,这里需要强调一下,逻辑简单的类,其实没必要写单测,一般只是领导要求纯粹的追求覆盖率的时候,才会把这种简单的类补上去。

  举个很简单的例子: 

  studentService.getStudentById(Long id) ,我相信你都能脑补里面的逻辑,你要说你就想为这样的方法写单测,这当然可以,但是收益不大。

  单测收益最高的就是针对那些复杂的场景,比方说在开发周期比较紧急的时候,核心的、容易出错的逻辑才是更应该去重视的地方。(要是开发周期空闲,你要补哪都行)

  回到单测难写的问题上,用专业术语来讲,就是 你写的代码可测试性不高 ,导致难以编写对应的单测类。

  怎样的代码是可测试性不高呢?我举个非常简单的例子:

  假设你要给 garbageMethod 写个单测,是不是有点难?

  里面用到了静态方法,又 new 了个service。

  这静态方法我想让返回值等于 111,我只能去研究里面的逻辑。有人可能想不就是一个方法的逻辑吗,就看看呗。

  那就看看:

  可能你会说,这两分钟我就看明白了,但是这才一个,要是好多都需要看呢?

  你为了测试当前的方法,且花了一堆时间去理解别的不需要测试的类的逻辑,这做法本身就不符合逻辑。

  然后那个 noSevice 是 new 的,这如何控制它的返回值啊?我想 mock 这个类也替换不了啊!

  所以,这样的代码就是可测试性低的代码,不好 mock (当然,mock 框架支持静态方法的 mock,不过new noSevice 不好弄,当然一般人都有不会这样写的,我只是为了举例)

  还有各种类之间有继承关系的,这种测试难度都比较大。

  就是上面的种种原因,导致我们的单测难以编写。

  所以如果我们在设计接口的时候,先编写单测,我们写出来的代码其实可测试性就很高了,因为你完全晓得这样的写法会使得你单测很难进行下去,自然而然你写的代码就会往解耦的方向发展(比如上面的 noService 肯定会注入)。

  我来列举下具体哪几种代码写法使得我们单测难以编写:

  1. 静态方法(不好mock替换注入,不过现在mock框架已支持)

  2. 内部直接 new ,强依赖,无法 mock 替换注入

  3. 继承类,测试当前类的方法逻辑,还需要关心父类逻辑和mock父类的服务(所以我们常说组合优于继承)

  4. 全局变量,这个应该好理解,好方法都公用,你改了值之后,会影响别的测试类,特别是并发执行测试类时,就傻了

  5. 时间等一些未决行为,代码里面有 new Date,逻辑是近 15 天可行,然后超过 15 天就跑不通了(当然可以通过动态计算时间)

  这里我要强调下,我不是说上面的这几种代码不能写,这是不现实的,我只是列举说明这几种可能会使得你的单测不好写, 当然第 2 点就是不能写的 。

  写个单测例子

  说了那么多,不如实战一下,我就拿 trainingYes 来举例说明,这里引入 mockito 测试框架。

  可以看到,通过注解 mock 了需要 mock 的 dao 和 provider ,然后将其注入到我们要测试的 yesService 中。

  接下来就是具体的逻辑,根据场景我一共写了 4 个方法来测试:

  里面的 when(xxxx).thenReturn(xxx) ,就是我们指定的 mock 逻辑,这就是指哪打哪,随心所欲。

  我们跑一下,你看就很快,59 ms,也不需要 Spring 框架。

  就是通过这样的 mock 手段,忽略了依赖的服务的逻辑,使得我们要它怎样就怎样,便于我们单测类的编写。

  至于具体的 mockito 的使用方式,这篇就不做展开了,网上看看应该简单的。

  然后上面提到的静态方法的模拟,也简单的,我截个网上的例子:

  上面的逻辑就是模拟静态方法 StaticUtils.name ,跟普通对象不同的是它用完之后需要 close 一下,所以用了 try-with-resource,当然也可以手动 close,原理也不做展开,有兴趣的小伙伴可以自己去了解下。

  看到这,想必你对单测应该已经挺有感觉了吧?

  道阻且长

  知道了单测如何写和为什么难写之后,其实我们的思路已经清晰了,但是往往现实还是残酷的。

  以前的老代码,巨多,领导要求补,难!

  一个 service 依赖十几个服务,mock 都 mock 傻了,难!

  项目太紧急了,从长远来看,单测的收益会使得整体开发和后期维护的时间短,但是领导就是要求下周一上线,难!

  我个人认为一些稳定的代码,除非现在真的没事做了,完全没必要去补单测,完全可以在改动对应的点的时候再去补,然后新写的方法都要求上单测,这是非常合理的。

  如果写业务的时候,同步写单测,会促进你的思考,缕清思路,写出的代码因为可测试性高,自然而然就比较漂亮和解耦。

  还有一点也很重要,其实我们写单测的时候,不应该过多的关注内部的逻辑,举个非常简单的加法例子,我们单测只关心 add(1,1) 的结果是 2,我管你里面是的实现到底是位运算还是啥运算?

  因为只有当我们的单测没有过度的关心内部实现时,之后方法的具体实现变更(从普通的 +,变成了位运算),我们的单测才不需要进行对应的修改。

  但实际上这种情况对我们业务不太适用。

  举个例子 YesService 之前依赖 yesDao ,现在这个 Dao 被剥离了,变成了另一个 RPC 服务,对应的我们之前所有的测试用例还是需要更改的,这是没办法的事情。

  不过为什么我还要提一下这点呢?

  比如你的测试方法里面有个 xxxService.save 逻辑,这个方法没有返回值,后面的逻辑也不依赖它,那么就不要想着在单测是时候写 verify(xxxService.save(..)); 来验证这个方法是否被调用。

  这样验证是否被调用其实意义不是很大,并且之后如果 xxxService 被移除了,单测就抛错了,因为里面没有调用 xxxService.save ,你还需要把这个单测给修复了。

  这就是我所说的,写单测的时候,不要过度关注方法内部实现(有些需要mock的没办法)。

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

 

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取 

 

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

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

相关文章

Windows-WSL2-VSCode+Docker配置C++开发环境

Windows-WSL2-VSCodeDocker配置C开发环境 写在前面 因为在学习工作中,需要不同的编码环境,若将这些不同的开发环境都状态一台设备上,很容易出问题,而且迁移性差,于是计划把不同的开发环境用docker隔离开来&#xff0…

面向对象编程第一式:封装 (Java篇)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. 🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人…

PMP考试心得,与大家共勉

本人刚刚通过PMP考试,有一些自己的经历,写出来欢迎已经是PMP的兄弟们指正,希望能给正在PMP之路上奔跑的人们一些帮助。 其实很早就听说过PMP认证考试了,但是一直工作很忙没有时间来投入学习考试。因此一直拖到今年夏天才有时间参加…

耐腐蚀PFA气体洗涤瓶可多级串联透明特氟龙塑料氢气吸收装置

洗气瓶是一种常用于净化和干燥各种气体的实验室器皿,以去除其中的水分、油脂、颗粒物等杂质,从而使需要用到的气体满足实验要求。 PFA洗气瓶的工作原理: 主要是通过液体吸收、溶解或发生化学反应来去除气体中的杂质。在洗气过程中&#xff…

加速布局Wi-Fi及蓝牙市场!移远通信再推四款高性能模组新品

3月12日,全球领先的物联网整体解决方案供应商移远通信宣布,其已正式推出四款新型Wi-Fi和蓝牙模组新品,旨在继续致力于满足物联网行业不断升级的应用需求,为智慧家居、工业互联、储能、充电桩等各种场景提供一站式创新解决方案。 此…

Kotlin 空类型,区间,数组

目录 1. 空类型 2. 区间 3. 数组 1. 空类型 我们知道任何一种数据类型都有为空或不为空两种状态,在 Kotlin 中,若允许一个数据为空,则需要使用 "?",默认都不能为空。 代码举例说明 // 给 notNull赋值为空&#x…

2023.5.9工作问题记录————安卓GKI检测abi-check

GKI ABI-check SOP ABI check是用于校验生成的kernel中ko与google释放的ko所计算出来的CRC是否相等。 在GKI版本中,很多kernel中的配置有y变成了m,编译成了ko,XTS测试时,会替换boot镜像, 替换成google释放的boot.img&…

Vue3组件详情

Vue3组件详情 一、父组件向子组件传值 ref、props二、子组件向父组件传值 emit三、子组件向父组件传值 v-model四、setup语法糖1、基本用法2、data和methods3、计算属性 computed4、监听器 watch、watchEffect5、自定义指令 directive6、import导入的内容可以直接使用7、声明pr…

Flume集成Kafka

之前提到Flume可以直接采集数据存储到HDFS中,那为什么还要引入Kafka这个中间件呢,这个是因为在实际应用场景中,我们既需要实时计算也需要离线计算。 Kfka to HDFS配置 # Name the components on this agent a1.sources r1 a1.sinks k1 a1.…

动态规划7,等差数列划分,湍流子数组,唯一的子字符串,最长递增子序列

等差数列划分 思路: 经验题目要求 dp[i]表示:以 i 位置为结尾的所有子数组中有多少个等差数列 状态转移方程 对 dp[i] 位置,数列至少有三个元素,如果相邻三个为等差数列,dp[i] dp[i-1] 1; 如果相邻三个不为等差数…

windows批处理脚本(cmd指令)

一、简介 最早期的电脑系统是DOS系统,DOS系统只有一个黑漆漆的窗口,需要自己输入命令,所以学习命令是很有必要的,那么CMD命令大全是什么?直到今天的Windows系统,还是离不开DOS命令的操作。如今懂得使用windows批处理脚…

【AI绘画教程】AI绘画图生图怎么用?

AI绘画技术已经越来越成熟,越来越多的人开始尝试利用AI进行创作。而AI绘画图生图作为一款优秀的AI绘画工具,正是帮助许多人创作的好帮手。 AI绘画图生图功能可以通过多种软件实现,具体的操作步骤可能因软件而异,但大体流程相似。以…

基于springboot+vue的会议室预约系统(源码+论文)

目录 前言 一、功能设计 二、功能实现 三、库表设计 四、论文 前言 随着互联网技术的发展,各行各业乃至人们的衣食住行都离不开网络。就拿最普普通通的衣食住行来说吧,穿衣服、买衣服我们现在基本都是在网络上进行购买,线下商场基本不去。…

1688商品详情数据采集(商品属性,规格,价格,详情图等)

京东商品详情数据采集是一个复杂但重要的过程,它涉及获取商品的详细信息,包括商品属性、规格、价格以及详情图等。以下是关于如何进行京东商品详情数据采集的基本步骤: 确定采集目标:首先,你需要明确需要采集的商品信…

ARM 汇编指令:(五)CMP指令

目录 1.CMP比较指令 2.指令条件码 cond 1.CMP比较指令 CMP指令是计算机指令集中的一种比较指令,用于比较两个操作数的大小关系或相等性,并根据比较结果设置或更新条件码寄存器(或程序状态字)的标志位。 指令格式:C…

VUE内盘期货配资软件源码国际外盘二合一

开发一个Vue内盘期货配资软件源码,同时兼容国际外盘二合一的功能,是一个复杂且专业的任务,涉及前端Vue.js框架的使用、后端服务器处理、数据库管理、实时交易接口对接等多个方面。下面是一些关于开发此类软件的基本指导和考虑因素&#xff1a…

什么是同城上门预约按摩系统,上门预约平台有哪些功能?

随着互联网技术的发展,人们的生活方式发生了很大的变化。在日常生活中,大家都习惯使用手机来订餐、购物、家政服务等,这也为我们的生活带来了很大的便利。而同城按摩小程序作为一种新兴的按摩预约方式,受到了越来越多人的欢迎。下…

机器视觉检测设备的组成要素

机器视觉检测设备是一种先进的自动化检测技术工具,它利用光学、图像处理和计算机硬件及软件技术模拟并扩展人类的视觉功能,以实现对产品或目标物体进行自动化的尺寸测量、缺陷检测、表面质量评估、颜色识别、形状匹配以及位置判断等功能。这种设备通常包…

c/c++| 常规 |sizeof 、strlen

总结来说 ,sizeof 查看内存给对象分配的空间大小,不仅仅是普通的内置变量,还包括用户自定义变量、结构体、类对象 然后strlen 是查看字符串的实际长度大小,注意它不会计算那个结束符’\0’

重生奇迹MU攻击防御技能石哪里掉

在《重生奇迹MU》中,攻击和防御技能石可以从以下途径获得: 1.怪物掉落:你可以通过击败怪物获得攻击和防御技能石,不同的怪物掉落不同的石头。你可以在各个地图的怪物掉落表中查看特定怪物掉落的技能石。 2.商店购买:…