教你该如何写单元测试

news2024/11/17 21:39:49

目录

前言:

到底什么是单元测试

为什么单测这么难写

写个单测例子

道阻且长


前言:

编写单元测试是软件开发中非常重要的一环,它可以确保代码的质量,减少Bug的产生,提高代码的可维护性,同时也能够大大提高开发效率。

到底什么是单元测试

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

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

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

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

具体来说就是一个 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的没办法)。

  作为一位过来人也是希望大家少走一些弯路,希望能对你带来帮助。(WEB自动化测试、app自动化测试、接口自动化测试、持续集成、自动化测试开发、大厂面试真题、简历模板等等),相信能使你更好的进步!

留【自动化测试】即可【自动化测试交流】:574737577(备注ccc)icon-default.png?t=N4P3http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=2szVdJcy6VnqVi_zYhQd8aI9U5yfUv34&authKey=leQfP2SBsSV1%2FUzpd2OtJhdk%2F0SH%2FzEdi8uCVyM4q8w%2FHQEA1WUh3aqS9kyXZxUH&noverify=0&group_code=574737577

 

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

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

相关文章

ts自定义迭代器

key 为 [Symbol.iterator]

软考A计划-系统架构师-案例分析知识点整理

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列 👉关于作者 专注于Android/Unity和各种游戏开发技巧&#xff…

【数据结构】24王道考研笔记——栈、队列和数组

三、栈、队列和数组 目录 三、栈、队列和数组栈基本概念顺序栈链式栈 队列基本概念顺序存储链式存储双端队列 应用括号匹配前中后缀表达式栈在递归中的运用队列的运用 数组数组的存储对称矩阵三角矩阵三对角矩阵稀疏矩阵 栈 基本概念 栈是只允许在一端进行插入或删除操作的线…

朋友拿下字节27K的offer,实名羡慕了....

最近有朋友去字节面试,面试前后进行了20天左右,包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说,80%的人都会栽在第一轮面试,要不是他面试前做足准备,估计都坚持不完后面几轮面试。 其实&…

Redux异步解决方案 1. Redux-Thunk中间件

简单介绍一下thunk,这是一个中间件,是解决redux异步问题产生的。我们都知道,在使用redux的时候,通过dispatch一个action 发生到reducer 然后传递给store修改状态 一系列都是同步的,那如果说我dispatch一个action 这个a…

blockchain layer区块链分层

目录 1.layer0 2.layer1 3.layer2 ​4.layer3 1.layer0 第0层的定义目前行业还没有完全一致的理解。多数人认为第0层是 加密数据连接层及其硬件,对应上图下半部分。 也有一些人把跨链或可以创建链的基础设施为作为第0层,他们的代表有: LayerZero、S…

一文讲清后摩尔时代国产高性能并行应用软件生态建设

摘自《后摩尔时代国产高性能并行应用软件生态建设综述》 作者: 龚春叶1,刘杰1,包为民2,潘冬梅1,甘新标1,李胜国1 陈旭光1,肖调杰1,杨博1,王睿伯1 (1.国防科技大学 并…

基于stm32作品设计:多功能氛围灯、手机APP无线控制ws2812,MCU无线升级程序

文章目录 一、作品背景二、功能设计与实现过程三、实现基础功能(一)、首先是要选材(二)、原理图设计(二)、第一版本PCB设计(三)、焊接PCB板(四)编写单片机程序…

软件测试基础知识 —— 白盒测试

白盒测试 白盒测试(White Box Testing)又称结构测试、透明盒测试、逻辑驱动测试或基于代码的测试。白盒测试只测试软件产品的内部结构和处理过程,而不测试软件产品的功能,用于纠正软件系统在描述、表示和规格上的错误&#xff0c…

基于诺亚无人船ROS与Dronekit之间的通信浅析

阿木实验室的诺亚无人船上市已经有一段时间,经过对开发者们的多次调研,我们发现不少开发者都对诺亚无人船的通信实现方式感兴趣,为了帮助大家更好地理解并使用该产品,本期我们将针对诺亚无人船中所使用的linux编程技术以及ROS系统…

2023最新互联网工程师 Java 面试题及答案整理(7 天就能吃透)

现在 Java 面试都只是背答案吗? 不背就通过不了面试,但是现在面试都问原理、问场景!Java 面试题就像我们高考时的文言文,包括古诗词,不背是不可能答出来的!当然了,除了背,还得理解&…

某球中如何驾驶西锐SR-22小飞机在美国大峡谷中穿行

某球中如何驾驶西锐SR-22小飞机在美国大峡谷中穿行 我已经厌烦了无聊的围绕机场的五边飞行了,想飞一趟跨越乡野的转场飞行了。在我常用的飞软SimplePlanes里面,我已经完成取胜了所有的竞速赛道,我想自己创建一个航路想定,最终选择…

帆软Finereport数据分页,分页查询

目标: 在数据集中一次性获取所有数据后,分页查看,导出时导出的所有数据 如图: 实现步骤: 一、在表格中点击第一列数据集的单元格,添加条件属性, 条件属性内容:&A3 % 5 0 公式解…

【Spring框架学习】了解什么是Spring框架?Spring框架有什么用?创建第一个SpringBoot项目

前言: 💞💞今天我们开始学习Spring,这里我们会了解什么是Spring,知道什么是框架,为什么要学Spring框架,框架有什么作用等等。 💟💟前路漫漫,希望大家坚持下去…

pikachu靶场-../../(目录遍历)

目录遍历, 也叫路径遍历, 由于web服务器或者web应用程序对用户输入的文件名称的安全性验证不足而导致的一种安全漏洞,使得攻击者通过利用一些特殊字符就可以绕过服务器的安全限制,访问任意的文件 (可以是web根目录以外的文件),甚至…

客户案例:CACTER邮件安全网关解决餐饮企业邮件安全痛点,有效提升防护!

客户背景 某大型餐饮企业是一家在全国范围内拥有多家连锁店的知名品牌,以优秀的产品和服务质量,严格的质量控制和管理体系,以及开创性的营销策略,赢得了广泛的客户认可和信任。 然而,正因为该企业具有良好的口碑和声誉…

sonar scanner配置

sonar scanner配置 这里记录如何配置sonar scanner扫描C/C项目代码。话不多说,先上官网链接。 文章目录 sonar scanner配置1. 环境1.1 SonarSource Build Wrapper1.2 sonar-scanner 2. 使用2.1 Compilation Database2.2 执行sonar-scanner 3. 注意 1. 环境 对于C…

记录一下CSDN的markdown新功能

新功能目录 CSDN Markdown更新了欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如…

什么是原型设计?入门最全讲解指南

原型设计在产品开发和用户体验领域扮演着至关重要的角色,产品经理通过画产品原型图,可以让需求可视化,进而快速测试和验证产品可行性,为后续推动产品研发提供坚实可靠的依据。 本文将深入探讨什么是原型设计,原型设计…

【深度学习】0-1 深度学习相关数学概念的简单总结-线性代数

线性代数 标量(scalar) 标量就是一个单独的数,只具有数值大小,而没有方向,部分有正负之分。一般用小写的变量名称表示,如a、x等。 向量(vector) 一个向量就是一列数,这…