您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

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

时间:2022-07-20 11:50:48  来源:  作者:小柴学Java

到底什么是单元测试

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

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

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

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

具体来说就是一个 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啥玩意啊,是人写的吗?

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

如此往复,年复一年。

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



Tags:单元测试   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Android开发中如何进行单元测试?
单元测试介绍单元测试是软件开发中的一种测试方法,用于验证代码中的最小可测试单元(通常是函数或方法)是否按预期工作。单元测试通常由开发人员编写,旨在隔离和测试代码的特定部...【详细内容】
2023-12-11  Search: 单元测试  点击:(170)  评论:(0)  加入收藏
三个要点,掌握Spring Boot单元测试
单元测试是软件开发中不可或缺的重要环节,它用于验证软件中最小可测试单元的准确性。结合运用Spring Boot、JUnit、Mockito和分层架构,开发人员可以更便捷地编写可靠、可测试...【详细内容】
2023-09-28  Search: 单元测试  点击:(265)  评论:(0)  加入收藏
单元测试的重要性:编写更安全、更可靠的代码
在软件开发过程中,测试是非常重要的一环。而在众多的测试方法中,单元测试占据了不可忽视的地位。那么,为什么我们需要进行单元测试呢?以下将从理论和实践两方面进行详细的解释。...【详细内容】
2023-09-20  Search: 单元测试  点击:(247)  评论:(0)  加入收藏
掌握Python中的单元测试:详尽指南与Unittest
单元测试是一种测试方法,用于验证软件中最小可测试单元(如函数、方法或类)的行为是否符合预期。它有助于确保代码的质量、可靠性和可维护性。让我们以一个简单的示例来说明如...【详细内容】
2023-09-01  Search: 单元测试  点击:(207)  评论:(0)  加入收藏
Java 单元测试及常用语句
1 前言 编写 Java 单元测试用例,即把一段复杂的代码拆解成一系列简单的单元测试用例,并且无需启动服务,在短时间内测试代码中的处理逻辑。写好 Java 单元测试用例,其实就是把 “...【详细内容】
2023-08-31  Search: 单元测试  点击:(359)  评论:(0)  加入收藏
简化Java单元测试数据
作者 | 张哲EasyModeling 是我在2021年圣诞假期期间开发的一个 Java 注解处理器,采用 Apache-2.0 开源协议。它可以帮助 Java 单元测试的编写者快速构造用于测试的数据模型实...【详细内容】
2023-07-28  Search: 单元测试  点击:(289)  评论:(0)  加入收藏
该死的单元测试,写起来到底有多痛?
到底什么是单元测试这个问题看似非常简单,单元测试嘛,不就是咱们开发自己写些测试类,来测试自己写的代码逻辑对不对。这句话没有问题,但是不够准确。首先我们要明白,这个测试二字...【详细内容】
2022-07-20  Search: 单元测试  点击:(309)  评论:(0)  加入收藏
软件测试知识点3大场景带你了解单元测试
介绍怎么做单元测试的书很多,这里主要解答:为什么单元测试。客观来说,单元测试和使用版本控制系统(GIT,SVN)是一样重要的。为什么单元测试如此重要,但你却感受不到。首先要知道,代...【详细内容】
2021-05-18  Search: 单元测试  点击:(426)  评论:(0)  加入收藏
单元测试 vs 集成测试,你该怎么选?
在 1998 年,Kent Beck 编写了 sUnit,一个面向 SmallTalk 的单元测试框架。之后,他将这个框架移植到 Java,即 jUnit。从那时起,xUnit 框架扩展到那些最流行的编程语言。比较新的语...【详细内容】
2021-01-14  Search: 单元测试  点击:(491)  评论:(0)  加入收藏
从零开始写单元测试
关于单元测试的三个问题作为一个程序员,或多或少听说过单元测试,但很多小伙伴还没有在实际项目中用到。究其原因,可能是对单元测试有一些「误解」,比如: 写单元测试需要花费更多...【详细内容】
2020-10-10  Search: 单元测试  点击:(229)  评论:(0)  加入收藏
▌简易百科推荐
Meta如何将缓存一致性提高到99.99999999%
介绍缓存是一种强大的技术,广泛应用于计算机系统的各个方面,从硬件缓存到操作系统、网络浏览器,尤其是后端开发。对于Meta这样的公司来说,缓存尤为重要,因为它有助于减少延迟、扩...【详细内容】
2024-04-15    dbaplus社群  Tags:Meta   点击:(3)  评论:(0)  加入收藏
SELECT COUNT(*) 会造成全表扫描?回去等通知吧
前言SELECT COUNT(*)会不会导致全表扫描引起慢查询呢?SELECT COUNT(*) FROM SomeTable网上有一种说法,针对无 where_clause 的 COUNT(*),MySQL 是有优化的,优化器会选择成本最小...【详细内容】
2024-04-11  dbaplus社群    Tags:SELECT   点击:(3)  评论:(0)  加入收藏
10年架构师感悟:从问题出发,而非技术
这些感悟并非来自于具体的技术实现,而是关于我在架构设计和实施过程中所体会到的一些软性经验和领悟。我希望通过这些分享,能够激发大家对于架构设计和技术实践的思考,帮助大家...【详细内容】
2024-04-11  dbaplus社群    Tags:架构师   点击:(2)  评论:(0)  加入收藏
Netflix 是如何管理 2.38 亿会员的
作者 | Surabhi Diwan译者 | 明知山策划 | TinaNetflix 高级软件工程师 Surabhi Diwan 在 2023 年旧金山 QCon 大会上发表了题为管理 Netflix 的 2.38 亿会员 的演讲。她在...【详细内容】
2024-04-08    InfoQ  Tags:Netflix   点击:(5)  评论:(0)  加入收藏
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(9)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(16)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(14)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(9)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(15)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(10)  评论:(0)  加入收藏
站内最新
站内热门
站内头条