6

聊聊单元测试

 3 years ago
source link: https://zacharyfan.com/archives/1367.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

聊聊单元测试

这里是Z哥的个人公众号

每周五11:45 按时送达

当然了,也会时不时加个餐~

我的第「167」篇原创敬上

大家好,我是Z哥。

提起单元测试,很多人对它的态度是,我知道它有用,但是我不想写。大多数人的理由是没时间写,任务太多。

但是说实话,是真的没时间吗?Z哥认为真是由于没时间而不写单元测试的人绝对是少数。况且,导致没时间很大原因可能就是花了太多时间在处理bug上。

所以,很多人没有把单元测试当作一个“工具”,而把它看作是一种“负担”。

在这种心态下,就算要写单元测试,也是为了写而写。更可怕的是,通过mock工具,还真能给任意代码写单元测试。

但是这样的做法其实是“买椟还珠”,真正的浪费时间。

最典型的情况是,很多人一开始写测试代码就错了,看上去写了很多Mock、Assert,但是到底想验证什么,测试什么其实并不明白。一个不留神,测试代码就变成验证某个RPC接口对不对,某个第三方系统库的函数对不对等等,这明显就跑偏了。

对于这种情况,无论多么牛逼的工具都帮不了你,只能提高自己对单元测试的理解。

还有一种情况是,写代码的时候并没有考虑这代码要怎么测,因此写完了以后发现写单元测试很难,没有现成的测试入口。这时候项目交付的deadline又快到了,唉,要不先放着改天再写吧。当然我们都知道,这个改天大概率再也不会做。

我们有一万个理由可以不做单元测试。但是这就好比,组装一架飞机不用测试各个零件的运作是否符合预期,直接让它飞起来再看有哪些问题。

以后如果谁说单元测试不重要的时候,你不妨问他“你敢坐没有检查过零件的飞机么?”

另外,单元测试除了对软件质量有提升外,对软件的开发效率提升也很明显

在《实用软件度量》一书中提到了微软内部的统计数据,单元测试的成本效率是系统测试的3倍。

在《单元测试的艺术》中也提到过一个案例,找了开发能力相近的两个团队,同时开发相近的需求。进行单测的团队在编码阶段时长增长了一倍,从7天到14天,但是,这个团队在集成测试阶段的表现非常顺畅,bug量小,定位bug迅速等。最终的效果,整体交付时间和缺陷数,均是单元测试团队最少。

单元测试还有一个好处,就是让我们嘴上说的写「高内聚低耦合」的代码有了一条统一的实现路径。因为代码到底算不算高内聚低耦合,其实每个人的主观标准都不同。但是是否容易做单元测试,这却是一个相对更客观的标准。

所以,如果有人跟你说他这段代码设计得非常好,但就是不好写单元测试,相信你知道该怎么做了:D

那么,正儿八经的单元测试应该怎么写呢?我来分享一些我的经验和思考,希望能让更多的人参与到编写单元测试的队伍中来。

/01 怎么才算“单元”?/

相信很多人和Z哥一样,刚接触单元测试的时候觉得单元测试就是用来测某个方法的。其实并不是这样,这里的「单元」如何定义取决你如何定义“一件事”。只要这个「单元」里做的是“同一件事”,那么哪怕其中包含了3个方法,它也可以是一个「单元」。

比如,你写了一个下订单的单元测试,你可以把生成订单方法和扣减红包方法放在一起做单测,这样比两个方法分别做单测还可以多做一些关联验证。比如,订单上的红包金额是否与扣减的红包金额一致?

/02 如何判断单元测试的好坏/

单元测试和大多数技术工作不同,写得越好的单元测试往往用到的工具越简单,甚至不需要额外的工具。

在我的概念里,单元测试的好坏分为以下几个等级。

第一级,大部分代码不需要 Mock 就可以测试。这是最优秀的。
第二级,大部分代码需要Mock才能测试,但都不是静态方法。
第三级,大部分代码需要Mock才能测试,而且包含大量静态方法。(一般的Mock工具还无法Mock静态方法)

可能你会有疑问,为什么Mock静态方法是不好的?这个后面讲具体做法的时候会说。

说了这么多,具体怎么写呢?写单元测试其实就是做以下三件事。

/01 确定写单元测试的范围/

做任何的事都得回归到价值本身,单元测试也是如此。比如,你给一个固定返回字符串“Hello World”的方法写单元测试就是一个浪费时间的事情。

一般来说,哪些类型的代码适合写单元测试?

公用组件库。这些代码变更不会特别频繁,所以覆盖率需要尽量达到100%。

被调用频次越高的代码。

/02 怎么写?/

具体怎么写其实就是确定你要通过代码验证的东西是什么。这里你可以根据以下这4个标准来,不同重要度的方法,可以选择适合的标准来写。

  • L1:输入正确的参数时,会有正确的输出。(测试正确的处理逻辑是否符合预期)
  • L2:输入错误的参数时,不能抛出系统级的异常。(测试错误的处理逻辑是否符合预期)
  • L3:极端情况和边界数据可用。可能一开始无法考虑到很多边界条件和极端情况,所以这是一个需要长期维护的部分。
  • L4:覆盖率达到100%。

Z哥我对这4个标准的运用场景是:

  • L1,实在时间紧迫并且代码对应的功能不是核心部分。
  • L2,非核心模块大部分时候应该要达到的标准。
  • L3,核心模块要达到的标准。
  • L4,全局基础框架、封装的非业务型类库要达到的标准。

/03 单元测试的数据从哪来?/

很多人觉得写单元测试麻烦,主要的原因就是觉得构造测试数据费时间。所以,取巧的方法是直接连到DB,基于DB里的数据做单元测试。

但是这样的数据是不稳定的,一旦某个前置方法的逻辑有问题,导致数据库里的数据出现异常,那么后续的测试方法都会连续出错。

所以我认为单元测试的测试数据应该人为地在测试代码里构造。如此不但能让数据变得稳定,而且单元测试的运行效率也会更高,毕竟少了多次连接数据库的操作。

《Google软件测试之道》中提到谷歌的做法也是如此。在谷歌,单元测试被划分为「小型测试」类型,对于小型测试的特点就是不需要外部依赖,所以涉及到的外部服务需通过Mock或Fack来实现。(Mock、Fake、Stub都是单元测试中的基本概念,可以自行搜索了解)

再分享两个最佳实践给你,让你可以更容易编写单元测试。

涉及到I/O的代码和业务代码尽量分开。这里的I/O不仅仅是磁盘I/O还有网络I/O。

Pascal之父——Niklaus Wirth提出过一个著名的公式:程序 = 算法 + 数据。数据的操作和获取就是通过I/O进行的,一旦剥离后,剩下的代码就是算法,也就是“逻辑”,我们写单元测试要验证的恰好就是它。

实现方式也很简单,将I/O部分抽象出接口,通过依赖注入方式调用。这样你在写单元测试的时候可以通过Mock方式来提供一个I/O方法的实现。

测试数据与用例分离。在你写单元测试的时候,因为需要考虑很多种情况,所以需要构造好几套测试数据。

为了便于管理和维护这些数据,你可以避免将数据与单元测试代码写在一起。举个例子,你可能平时是这么写的。

@Test
Public void testAdd(){
assertEquals(expect: 2, MethodAdd(a: 1,b: 1));
assertEquals(expect: 0, MethodAdd(a: -1,b: 1));
assertEquals(expect: 0, MethodAdd(a: 0,b: 0));
}

以后你可以试试这样写:

@Test
void testAdd(){
for(Object[] s : data()){
assertEquals(s[2], (int)s[0]+(int)s[1]);
}
}

public static Iterable<Object[]> data(){
List<Object[]> list = new ArrayList<Object[]>();
list.add(new Object[]{1,1,2});
list.add(new Object[]{-1,1,0});
list.add(new Object[]{0,0,0});
return list;
}

这样,后续维护测试参数只要在data()方法里进行就好了(当然你也可以使用junit之类的工具来简化这个写法)。毕竟做单元测试是一件长期的事情,需要根据新发现的bug保持测试数据的更新,以确保已发生的bug总是被覆盖在单元测试范围内。

另外,易于做单元测试的代码,其实它的性能也会不错。因为耗时的I/O操作不会隐藏在各个方法里,让你无意间就重复调用了。相反,你可以直观的看到每个方法里有哪些I/O操作,能合并请求的可以在调用这些方法之前合并掉。

好了总结一下。

这篇呢,Z哥和你分享了我对写单元测试这件事的看法。

首先,我们应该把它当作“工具”而不是“负担”。因为单元测试除了可以提升软件质量,还可以提高开发效率,以及优化代码设计。

然后,实际在做的时候,我从「确定写单元测试的范围」、「怎么写?」、「单元测试的数据从哪来?」三个方面给了我的建议。并且分享了两个有效的最佳实践给你。

以我的亲身经历告诉你,当你每次改完代码run一遍单元测试,看到那些success和failurel列表的时候,你会觉得“真香”。不信你试试。

如今,好像一个团队不说自己在敏捷开发就落伍了。然而类似于测试驱动开发(TDD)之类的开发方式恰恰是敏捷开发实践的重要组成部分,但是我们却嫌弃它拖慢迭代速度。

那么我们到底是不是在敏捷呢?


原创文章,转载请注明本文链接: https://zacharyfan.com/archives/1367.html

关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描二维码~

微信公众号

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK