单元测试,到底什么是单元测试,为什么单测这么难写

很多小伙伴想知道单测到底该怎么写,于是,文章就来了!

话不多说,发车!

来源于yes的练级攻略 ,作者是Yes呀

 

到底什么是单元测试

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

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

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

它要求我们的测试粒度,

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

 

最后

好了,说了这么多,相信你对单测应该有所了解了吧?

最重要的还是对单测有个正确的认识,然后掌握 mock 的技巧,写新方法的时候,尝试设计完接口后,先写下单测,慢慢的你就会有感觉了,在写单测时,你自然而然的会考虑到诸多边界值的处理,你写的代码质量也会提高,渐渐地就会感受到单测的好处。

很多公司单测之所以推行不下去,就是因为没有一个很好的宣讲,或者说对单测的系统介绍。

我相信大家都是在一年中的某个月份,领导在会上突然来了一句话:我们接下来要写单测!下个月覆盖率要达到50%!

然后大家就吭哧吭哧开始写了,写么又是抄网上的一些例子,把整个项目一起,就进行集成测试了,然后写着写着,有人把数据库改了,跑的好好地单测就挂了。

要么就是写死数据,这个月单测是行的,下个月就挂了。

也没有人告诉你这单元测试写的不对,咱不是说写在 test 包里面的代码就叫单元测试。

一开始气势汹汹,后面虎头蛇尾,这就是绝大公司执行单测的真实写照。

领导很心痛,为什么就推不下去,大家都这么不积极,这么没有主人翁精神吗?

下属头痛加手痛,这tm啥玩意啊,是人写的吗?

就这样,每年的某个时刻,你的领导都会突发开始抓单测,然后持续几周或一个月,热情逐渐消退,最后无人问津,领导也假装不知道

如此往复,年复一年。

我们每天过的日子,好像也是如此?

··················END················

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

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

相关文章

Linux 学习和教训

今天在学习Linux的时候,突然脑抽风,在根目录下执行了这样一条命令rm -rf *当时就觉得空气凝固了。。。那时也没有想到可以用数据恢复软件恢复。直接就重启了。重启之后发现,就去就直接是grub>晕菜。。突然间想到可以时候救援模式&#xff…

WinForm(八)窗体,窗体

我们在控件那篇文章里说过,窗体和控件都是一个类,项目中一个个窗体,都是Form类的子类。关于这个类有几个重要的成员,也是最常用成员,以供初学者了解:Load事件:发生在构造函数后,Show…

java8

实验总结 没问题 代码托管 https://git.oschina.net/shuoge/java8 转载于:https://www.cnblogs.com/haha-23333/p/6875325.html

抓包工具fiddler和wireshark对比

了解过网络安全技术的人都知道一个名词“抓包”。那对于局外人,一定会问什么是抓包?考虑到,大家的技术水平不一,我尽可能用非专业的口吻简单的说一下。 抓包就是将网络传输发送与接收的数据包进行截获、重发、编辑、转存等操作&am…

你被大数据“杀熟”过吗?怎么解决的?丨Q言Q语

点击关注 InfoQ,置顶公众号 接收程序员的技术早餐网友“廖师傅廖师傅”表示,他经常通过某网站订某个特定酒店的房间,长年价格在 380 元 -400 元。偶然一次,他从前台得知酒店淡季的价格在 300 元上下。他用朋友的账号查询也是 300 …

Blazor VS Vue

Vue——两分钟概述Vue 是一个JavaScript 框架。在其最简单的模式中,您可以简单地将核心 Vue 脚本包含在您的应用程序中,然后开始构建您的组件。除此之外,对于更复杂的应用程序,您可以使用 Vue 自己的 CLI 创建(并最终发…

SAP ECC EHP7 RFC 发布成WebService

http://www.cnblogs.com/mingdashu/p/6877622.html 1、说明介绍 本文将RFC发布成WebService的详细步骤 不介绍如何创建rfc。 2、WebService创建 2.1、调用创建命令 在RFC界面点击 实用程序-->更多实用程序-->创建WEB服务-->来自函数模块 2.2、定义Web Service 2.2.1、…

一文把RabbitMQ讲透了,佩服!

目录 背景 消息队列 | 消息队列模式 ①点对点模式 ②发布/订阅模式 | 衡量标准 RabbitMQ 原理初探 | 基本概念 | 工作原理 | 常用交换器 | 消费原理 | 高级特性 ①过期时间 ②消息确认 ③持久化 ④死信队列 ⑤延迟队列 | 特性分析 RabbitMQ 环境搭建 Rabbi…

完美完全卸载Oracle 11g数据库

Oracle 11g可在开始菜单中卸载,然后同时需要删除注册表中相关内容。 操作系统:windows10专业版。 卸载步骤: 1、停用oracle服务:进入计算机管理,在服务中,找到oracle开头的所有服务,右击选择停止…

【LeetCode】链表精选11题

目录 快慢指针: 1. 相交链表(简单) 2. 环形链表(简单) 3. 快乐数(简单) 4. 环形链表 II(中等) 5. 删除链表的倒数第 N 个节点(中等) 递归迭…

20172304 2017-2018-2 《程序设计与数据结构》第六周学习总结

20172304 2017-2018-2 《程序设计与数据结构》第六周学习总结 教材学习内容总结 本周学习了数组。 首先是数组元素,数组具有优越性因为它可以声明一个能容纳多个可访问值的变量。数组的数据具有索引而且是从零开始的。 其次是声明和使用数组,可以用“…

使用 K8spacket 和 Grafana 对 K8S 的 TCP 数据包流量可视化

前言如何知道 K8S 集群内 Pod 之间建立了哪些 TCP 连接?集群之间存在哪些调用关系?使用 k8spacket 和Grafana,你可以可视化集群中的 TCP 流量。了解工作负载如何相互通信,以及建立了多少连接,交换了多少字节&#xff0…

粒子系统(一):从零开始画一颗树

准备 IDE:VisualStudio 2017 Language:VB.NET / TypeScript 图形API:Win2D Github:[ UWP ] [ TypeScript ] 本文将向你介绍一种粒子系统(Particle System)模拟植物的简单方法。 第一节 移动 粒子按照某种规…

python 获取Dmidecode 输出的系统硬件信息

目的:熟悉利用python 分析文本的信息。分析的文件信息是通过dmidecode 工具抓取的系统硬件信息。本文结构:(1) 分析dmidecode 工具的输出信息结构(2) 分别用两种方式对dmidecode 输出的信息实现抓取,获取Manufacturer、Product Name和 Serial…

20165313 《Java程序设计》第七周学习总结

教材学习总结 1.下载安装MySQL数据库管理系统。 2.MySQL数据库基本操作。 3.利用JAVA程序对MySQL数据库系统进行查找,更新,添加和删除操作。 学习中的问题与解决方案 1.运行书上安装MySQL命令后命令提示行显示系统错误5 解决方案 以管理员身份运行 2.运行…

五:CentOS7安装出现Warning

U盘安装CentOS 7提示 “Warning: /dev/root does not exist, could not boot” 解决办法 想将旧电脑安装CentOS7系统以作学习之用,奈何安装时出现错误,错误图示如下: 经多方查找、分析得知可能是启动引导不正确。 用usb writer重新制作了系统…

微软和Canonical宣布适用于Ubuntu 22.04 LTS的原生.NET 6

微软和 Canonical 达成新的合作伙伴关系,宣布了 Ubuntu 22.04 LTS 主机和容器的原生 .NET 可用性。.NET 开发人员现在可以通过一个 “apt install” 命令从 Ubuntu 22.04 LTS 安装 ASP.NET 和 .NET SDK 和运行时Canonical 为 .NET 6 LTS 和 ASP.NET 运行时发布新的、…

TCP的连接状态标识 (SYN, FIN, ACK, PSH, RST, URG)

一、TCP的状态 在TCP层,有个FLAGS字段,这个字段有以下几个标识:SYN, FIN, ACK, PSH, RST, URG。 其中,对于我们日常的分析有用的就是前面的五个字段。 它们的含义是: SYN 表示建立连接,FIN 表示关闭连接…

MySQL性能优化总结

一、MySQL的主要适用场景 1、Web网站系统 2、日志记录系统 3、数据仓库系统 4、嵌入式系统 二、MySQL架构图 三、MySQL存储引擎概述 1)MyISAM存储引擎 MyISAM存储引擎的表在数据库中,每一个表都被存放为三个以表名命名的物理文件。首先肯定会有任何存储引…

Blazor University (45)依赖注入 —— 将依赖项注入 Blazor 组件

原文链接:https://blazor-university.com/dependency-injection/injecting-dependencies-into-blazor-components/将依赖项注入 Blazor 组件源代码[1]定义我们的依赖在注入依赖之前,我们需要创建一个。我们将使用古老的 ToDo 示例,但请放心&a…