如何写出有效的单元测试

什么是单元测试

《单元测试的艺术》中对单元测试的定义:

一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行校验。

单元测试几乎都是用单元测试框架编写的;只要产品代码不发生变化,单元测试的结果是稳定的。

为什么需要单元测试

在我看来,单元测试的意义可以总结如下三点:

  • 单元测试是保证你写的代码是你想要的结果的最有效办法
  • 单元测试帮我们塑造设计
  • 单元测试是最好的文档之一

单元测试描述了代码的预期行为,可以最有效地保证代码正确运行,减少代码缺陷;由于单元规模较小,当因为代码变更出现问题的时候,可以帮助我们快速定位问题;有单元测试覆盖的代码,让我们更有信心,敢于放心做代码重构;

写单元测试的过程往往伴随着代码重构,如果发现一段代码单元测试很难写,就需要反思我们的设计,进而重构促进代码设计的优化,帮助我们塑造设计;

同时单元测试也是一个最佳的、自动化的、可执行的文档;没有单测覆盖的代码,是很难被维护的。

什么是有效的单元测试

可读、可维护、可信赖、快速执行!

《单元测试的艺术》中描述优秀单元的特性:

它应该是自动化的,可重复执行;
它应该很容易实现;
它应该第二天还有意义;
任何人都应该能一键运行它;
它应该运行速度很快;
它的结果应该是稳定的(如果运行之间没有进行修改的话,多次运行一个测试应该总是
返回同样的结果);
它应该能完全控制被测试的单元;
它应该是完全隔离的(独立于其他测试的运行);
如果它失败了,我们应该很容易发现什么是期待的结果,进而定位问题所在。

可读性

“一般程序员写得出计算机能读懂的代码。优秀程序员写得出人能读懂的代码” — 马丁·福勒

可读的代码才是可维护的;难以阅读和理解的测试用例,最终的结果就是删掉它,因为维护成本过高。可读性高于纯粹的性能。

可维护性

团队内使用一套范式的结构,有助于使之更好用,快速定位问题;消灭代码中的坏味道。

可信赖

可信赖的含义:

  1. 测试可重复;
  2. 测试与依赖环境隔离;
  3. 只测试不进行验证是不可靠的测试;
  4. 在测试类中不要依赖与测试的顺序;
  5. 测试的结果是精准的:校验的精准以及错误问题的精准定位;

快速执行

保证单测快速执行,缩短反馈时长;

为什么有效的单元测试如此重要

无效的单元测试是没有意义的,反而会增加维护成本,最终导致单元测试的失败!

如上图所示,坐标中任意一个点,其与横纵坐标垂直线所形成的矩形面积代表CI为团队带来的价值,那么在我看来有两个关键的因素:横坐标是单元测试的基础能力建设,纵坐标则是有效的单元测试;

没有有效的单元测试,基础能力做出花来也毫无意义!完善的基础能力同时也帮助我们更低成本的写出有效的单元测试。

如何写有效的单元测试

我们以Flutter为例,来一起讨论如何写有效的单元测试;

使用测试框架

Flutter官方提供的测试框架:

  • flutter_test
  • integration_test

统一的编码约定

不论是AAA(Arrange-Act-Assert)还是GWT(Given-When-Then),统一的编码约定帮助保证测试代码的可读性、可维护性。

使用测试替身

测试替身帮助我们隔离被测试代码,加速执行速度,保证测试代码是可信赖的;

  • Dummy:一种什么也不做的实现方式。接口中的每个方法什么也不做,如果方法有返回值,返回的值尽量接近null或者0。
  • Stub:Dummy的一种,Stub的函数并不返回null或0,而是返回能推动函数沿预定路径被测试的值。
  • Spy:Stub的一种,它返回测试所需的特定值,推动系统沿着我们期望的路径前行。然而,Spy能记住对它所做的事,并允许测试询问。
  • Mock:Spy的一种,它返回测试所需的特定值,推动系统沿着我们期望的路径前行,而且还会记住对它所做的事。不过,Mock还知道我们的预期,基于这些预期,判断测试是否通过;换而言之,Mock中写明了测试断言。
  • Fake:Fake是一种模拟器,它实现基础业务规则,这样测试就能要求该Fake按需要的路径执行。

一个测试应当只检查一件事

明确测试意图,一旦出错可以精准定位问题;

一个测试只有一个模拟对象

避免过多模拟对象,一个测试用例的校验内容尽量简单;

避免冗余测试

冗余测试会提高维护成本;

避免条件逻辑

条件逻辑会让你的单元测试更难以维护,出问题不容易排查,不够精准;

单测需要确定性

避免脆弱测试,Mock不确定的依赖:时间、随机数、并发性、基础设施、现存数据、持久化、网络等等;

测试快速执行

避免sleep等操作,导致测试执行缓慢;

避免过度指定

对于过度指定的讨论,其核心问题就是要我们判断哪些是单元测试应该覆盖的,哪些是应该留给其他测试手段的。如果一个场景,单元测试覆盖之后,导致经常单测失败,需要不断更新维护,那就可以考虑不做单元测试覆盖。

像素完美是一个典型的、经常拿出来讨论的例子,Flutter的Golden Test就是一个golden master testing的例子;《有效的单元测试》中关于像素完美的讨论:

像素完美:顾名思义,是一种特定于图形和图像生成的测试坏味道。它混杂了魔法数字和基本断言,使得测试极难阅读也极其脆弱。
这种测试几乎无法阅读,因为即使测试在语义上是处于高层概念的,却仍然会针对硬编码的底层细节例如像素坐标和颜色来进行断言。指定坐标上的像素是黑还是白,与两个图形是否相连或堆叠的概念是有区别的。
这种测试极其脆弱,因为即使很小的和不相关的输入变化——是否是另一个图像,或图形对象的渲染方式——都足以影响输出、打破测试,谁让你非要精确地检查像素坐标和颜色呢。同样的问题在采用golden master技术时也会遇到,其做法是事先将图像录制下来,并手工检查其正确性,以后再进行测试时就将渲染出的图像与之进行比对。
这些可不是我们愿意去维护的测试。我们不希望带着这种脆弱的精确度去编写测试,而是使用模糊匹配和智能算法来代替繁琐的数值比较。

对于特定场景,Golden Test是一个非常有效的手段,但需要非常谨慎的评估;慎用Golden Test!

不要写永不失败的测试,不要写没有校验的测试

单测需要对明确的逻辑校验,永不失败的测试或者没有校验的测试是不可信赖的。

测试不要名不副实

避免测试的描述与测试内容不符;测试结果必须精准;测试该失败的时候一定要失败!

测试私有或者受保护的方法

解决思路:

  1. 将方法变成公共方法;
  2. 将方法抽取到新类;
  3. 将方法变成静态方法;
  4. 将方法成为测试可见方法;

避免强制的测试顺序

依赖测试顺序导致测试可靠性变得脆弱,未来维护成本变高;

清理测试环境

在teardown阶段清理测试环境,例如还原全局的Config、清理创建的文件目录等等;

统一的单测命名、变量命名

统一的单测命名可以提高可读性、可维护性;

使用有意义的断言

断言的错误信息要有意义,出现问题能够明确错误的原因;

把单元测试视为“一等公民”

测试用例应该被视为“一等公民”:同样需要代码评审,同样需要代码质量检查,确保单元测试的有效性;

单元测试代码评审的过程,也是团队同学互相学习的过程,沉淀最佳实践的过程。

加速执行速度

日常对单测执行时间进行监控,对测试进行性能分析,优化执行时间过长的测试用例。

测试金字塔

测试金字塔是Mike Cohn在他的著作《Succeeding with Agile》一书中提出了这个概念。测试金字塔是一个比喻,它告诉我们要把软件测试按照不同粒度来分组。它也告诉我们每个组应该有多少测试。

为了维持金字塔形状,一个健康、快速、可维护的测试组合应该是这样的:写许多小而快的单元测试。适当写一些更粗粒度的测试,写很少高层次的端到端测试。注意不要让你的测试变成冰淇淋或者沙漏那样子,这对维护来说将是一个噩梦,并且跑一遍也需要太多时间。

避免测试重复

在实现测试金字塔时,你也应该牢记这两条基本法则:

  1. 如果一个更高层级的测试发现了一个错误,并且底层测试全都通过了,那么你应该写一个低层级测试去覆盖这个错误;
  2. 竭尽所能把测试往金字塔下层赶;

如果你已经在低层级测试里覆盖了所有情况,那么再维护一个高层级的测试就没有必要了。警惕沉没成本的思维陷阱,果断摁下删除键。没有理由在不再提供价值的测试上浪费宝贵时间。

补充单元测试应该从哪里开始

单元测试应该及时编写,就算没有实践TDD,也应该在代码实现之后尽快编写单元测试,避免写出不可测试的代码,也可以让bug尽早暴露;

但很不幸的,我们很多时候在刚开始卓越工程,推广单元测试的时候,不得不面对补充单元测试的情况;这绝对是一个有挑战的事情。

补充单元测试应该从哪里开始?参考测试金字塔,对于基础组件库来说,可以根据具体情况来定;

对于业务库来说,第一步建议从金字塔顶端的测试:

  • 优先覆盖回归测试用例中P0级别的用例;
  • 避免过度指定的端到端测试;
  • 适当的契约测试;

接下来,从金字塔中间层开始,不断向上、向下补充;

可测试的设计

应当容易、快速地为一段代码编写单元测试;可测试的设计,使我们写出模块化的设计;

行动指南

为了写出可测试的代码,需要注意以下几点:

  1. 避免复杂的私有方法;
  2. 避免final方法;
  3. 避免static方法;
  4. 使用new要当心;
  5. 避免构造函数中包含逻辑;
  6. 避免单例;
  7. 组合优于继承;
  8. 避免服务查找;
  9. 基于接口的设计;

可测试的代码是否违背了SOLID中的开闭原则?

可测试的代码设计,有的时候需要避免复杂的私有方法或者受保护的方法,因为这些意味着不可测试;

这样的话是不是意味着可测试的设计违反了开闭原则呢?

在代码重构的时候,可以认为给对象模型增加了另外一种最终用户——测试用户。

另外如果一部分代码实在不希望暴露,也可以使用@visibleForTesting 修饰;

单元测试与重构

写单元测试的过程往往伴随着重构;代码重构同样需要单元测试保证代码正确运行。

重构需要遵守的纪律:无测试重构无意义,频繁重构、果断重构、坚决重构;

持续重构

将麻烦扼杀在摇篮;

果断重构

敏捷编程的名言之一。规则很简单:重构时要勇敢。勇敢尝试,勇敢修改,不用害怕代码。

让测试始终能通过

建一个绿色安全区,不允许破窗出现。

留条出路

仓库打好tag,以便在需要的时候能够回滚。

可测试的代码

可测试的代码就是解耦了的代码;可测试的代码帮助我们实现更好的抽象。

做不到TDD,可以做到测试先行

下图是遵循TDD三大法则的实践过程;TDD很强大,但不一定适用所有的团队,推广难度很大,学习曲线很高。

TDD事实上由两个方面组成:测试先行,以及演进式设计;测试先行是非常重要的工程实践,做不到TDD,可以做到测试先行。

在Kent Beck的经典名著《解析极限编程》中,提到:尽早测试,经常测试,自动测试!

测试先行的本质能力要求是接口的设计能力——能否清晰的定义出设计单元的边界。

如何理解单元测试代码覆盖率

不要把它们变成管理的指标。
这就是你使用覆盖率数字的目的:使用它们作为衡量标准来帮助你改进,而不是用它们作为惩罚团队和使构建失败的棍棒。 ——《匠艺整洁之道》

代码覆盖率的一大忌讳:为了追求代码覆盖率,只测试不进行验证;

一味追求代码覆盖率,往往写出无效的单元测试,额外增加了维护成本,最终不得不放弃以失败告终。

与其追求代码覆盖率,不如将重点关注在确保写出有意义的测试。

沉淀最佳实践

必须承认单元测试有一定的成本,成本曲线来看,前期比较高;恰恰是这前期的门槛,让很多人望而却步。在团队内推广的时候,最难的就是写出第一个单元测试;

我们需要沉淀最佳实践,帮助降低写单元测试的成本,让我们更容易地写出有效的单元测试。

我觉得沉淀最佳实践最好的方法,就是Code Review;正如我们前面所说的,要把单元测试当成是“一定公民”,在Code Review的过程中,互相学习、分享最佳实践,消除无效的单元测试。

隔离单元测试与集成测试

集成测试是对一个工作单元进行的测试,这个测试对被测试的工作单元没有完全的控制,并使用该工作单元一个或多个真实依赖,例如时间、网络、数据库、线程或随机数生成器等。

任何测试,如果它的运行速度不快,结果不稳定,或者要用到被测试单元的一个或多个真实依赖,就是集成测试。

在日常开发过程,我们需要建一个绿色安全区:单元测试与集成测试隔离;

集成测试不够稳定,运行时间长等问题,如果不做隔离,日常开发浪费时间和精力维护,最后导致开发人员不再信任测试。

单元测试与ABTest

单元测试与ABTest有什么关系吗?事实上没有什么关系。但一定程度程度上,它们本质是相同的,都是保障线上代码质量(当然单测的成本,对于基建、开发者的能力的要求更高);

在日常开发中,经常主动为新的代码逻辑增加AB开关,一旦线上出问题留一条后路;发生问题的时候往往感慨AB开关救我一命;

单元测试可以让问题左移,防止问题上线,同样是一道保护;

如果有一天团队同学愿意主动增加单元测试来保护自己的代码,那么单元测试这件事就算比较成功了。

写在最后

从软件工程到卓越工程,单元测试从可选变成了必要;想要实现主干开发、大库模式,单元测试是前提条件。

关于单元测试这件事,我觉得最重要永远是写单元测试的人,优秀的团队文化非常重要,没有什么能够真正衡量单元测试做的好坏,有的只是程序员的职业操守。

我们花了很大的篇幅讨论有效单元测试的重要性以及如何写出有效的单元测试,不得不承认单元测试有一定的成本,真正实践依然需要很多的路要走,需要我们在实践中定义好单元测试的边界,找到最适合团队的最佳实践。

参考文档

  • 《单元测试的艺术》
  • 《有效的单元测试》
  • 《Succeeding with Agile》
  • 《匠艺整洁之道》
  • The Test Pyramid:https://martinfowler.com/articles/practical-test-pyramid.html
  • Software Engineering at Google:https://qiangmzsx.github.io/Software-Engineering-at-Google/#/zh-cn/Chapter-12_Unit_Testing/Chapter-12_Unit_Testing

作者 | 王浩(光酒)

原文链接

本文为阿里云原创内容,未经允许不得转载。

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

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

相关文章

测试环境不稳定复杂的必然性及其对策

这篇文章想要讲的,的确是两件事情: 为什么测试环境的不稳定是必然的,怎么让它尽量稳定一点?为什么测试环境比生产环境更复杂,怎么让它尽量简单一点? 此外,还会谈一谈对测试环境和生产环境的区别…

【计算几何】线段相交

问题描述:已知两条线段P1P2和Q1Q2,判断P1P2和Q1Q2是否相交,若相交,求出交点。 两条线段的位置关系可以分为三类:有重合部分、无重合部分但有交点、无交点。 算法的步骤如下: 1.快速排斥实验。 设以线段…

代码圈复杂度治理小结

网上有个段子,说建筑工程师不会轻易答应会给摩天大楼增加一个地下室,但代码开发工程师却经常在干这样的事,并且总有人会对你说“这个需求很简单”。到土里埋个雷,这确实不复杂,但我们往往面临的真实场景其实是“在一片…

MSE 治理中心重磅升级-流量治理、数据库治理、同 AZ 优先

本次 MSE 治理中心在限流降级、数据库治理及同 AZ 优先方面进行了重磅升级,对微服务治理的弹性、依赖中间件的稳定性及流量调度的性能进行全面增强,致力于打造云原生时代的微服务治理平台。 前情回顾 在介绍升级能力之前,先简要回顾 MSE 产…

基于阿里云 Serverless 快速部署 Function 的极致体验

1.Serverless 前世今生 1.1 Serverless 背景介绍 云计算的不断发展,涌现出很多改变传统IT架构和运维方式的新技术,而以虚拟机、容器、微服务为代表的技术更是在各个层面不断提升云服务的技术能力,它们将应用和环境中很多通用能力变成了一种…

性能提升1倍,成本直降50%!基于龙蜥指令加速的下一代云原生网关

​ 技术背景 网络信息传输的可靠性、机密性和完整性要求日渐提升,HTTPS 协议已经广泛应用。HTTPS 的 SSL/TLS 协议涉及加解密、校验、签名等密码学计算,消耗较多 CPU 计算资源。因此 CPU 硬件厂商推出过多种加速卸载方案,如 AES-NI、QAT、KA…

TiDB、OceanBase、PolarDB-X、CockroachDB 二级索引写入性能测评

为什么要做这个测试 二级索引是关系型数据库相较于NoSQL数据库的一个关键差异。二级索引必须是强一致的,因此索引的写入需要与主键的写入放在一个事务当中,事务的性能是二级索引性能的基础。 目前市面上的分布式数据库中,从使用体验的角度看…

EMQX + PolarDB-X 一站式 IoT 数据解决方案

本文整理自 EMQX 产品经理李国伟,在PolarDB开源社区中关于EMQX与PolarDB-X构建一站式IoT数据解决方案的分享。本篇内容主要分为四个部分: 1. IoT数据特性 2. EMQX介绍 3. EMQX与PolarDB-X集成 4. EMQXPolarDB-X方案DEMO 一、IoT数据特性 物联网应用场景…

阿里 Seata 新版本终于解决了 TCC 模式的幂等、悬挂和空回滚问题

大家好,我是君哥。 今天来聊一聊阿里巴巴 Seata 新版本(1.5.1)是怎么解决 TCC 模式下的幂等、悬挂和空回滚问题的。 TCC 回顾 TCC 模式是最经典的分布式事务解决方案,它将分布式事务分为两个阶段来执行,try 阶段对每…

10分钟部署一个别人可以访问的在线网站(文末有礼

你是否幻想过拥有自己的个人网站?但是不会编程,没有任何网站搭建经验,搭建的时候也不知道怎么去选择系统…… 等等这一系列疑惑让大部分人还没开始就选择放弃,本期教大家用一个最简单的方式,在10分钟内搭建一个线上的…

菜鸟 CPaaS 平台微服务治理实践

背景 CPaaS(cainiao platform as a service)是以公有云为基座,结合先进的云原生理建设的企业级 DevOps 的 PaaS 平台,CPaaS 主要目前主要支持的场景:菜鸟生态的云上研发运维、菜鸟公有云 SaaS 化的能力透出、菜鸟商业…

RocketMQ 消息集成:多类型业务消息-普通消息

引言 Apache RocketMQ 诞生至今,历经十余年大规模业务稳定性打磨,服务了 100% 阿里集团内部业务以及阿里云数以万计的企业客户。作为金融级可靠的业务消息方案,RocketMQ 从创建之初就一直专注于业务集成领域的异步通信能力构建。本篇将从业务…

【总结】字符串匹配: KMP 和 拓展KMP

比起ac自动机,kmp就一个next数组,理解了如何初始化next后就可以搞一些模板题了,下面是还不错的学习资料,清晰易懂,自己用的模板也来自它: http://chaoswork.com/blog/2011/06/14/kmp%E7%AE%97%E6%B3%95%E5%B0%8F%E7%BB%93/ kmp模板 next[0]-1;j-1; for(i0;i<m;) {while(j>…

最小生成树(普利姆算法、克鲁斯卡尔算法)

给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树. 求最小生成树的算法 (1) 克鲁斯卡尔算法 图的存贮结构采用边集数组,且权值相等的边在数组中排列次序可以是任意的.该方法对于边相对比较多的不是很实用,浪费时间. (2) 普里姆算法 图…

《数字化与碳中和(园区篇)》报告正式发布,助力加快推进国家“双碳”战略实施

2021年10月&#xff0c;国务院印发《2030年前碳达峰行动方案》&#xff0c;明确提出要建设绿色低碳园区&#xff0c;并选择100个具有典型代表性的城市和园区开展碳达峰试点建设&#xff0c;在政策、资金、技术等方面对试点城市和园区给予支持。此后&#xff0c;碳达峰、碳中和正…

基于开放共享的自主研发—MaxCompute 持续增强生态与开放性建设

MaxCompute产品与生态架构 MaxCompute是一个具有先进架构的Serverless云数据仓库&#xff0c;自从商业化后&#xff0c;使用的用户涉及各个行业的头部客户。在生态上需要支持主流的开源产品以及阿里云云产品。其主要包括以下几个方面&#xff1a; 数据接入生态。目前官方提供…

构建数据中台的组织架构

一、中台是一种企业架构 1.TOGAF企业架构标准 TOGAF是一套企业架构标准。企业架构是指整个公司或企业的软件和其他技术的整体观点和方法。企业架构又细分为业务架构、应用架构、数据架构、技术架构几个方向。 其中业务架构的定义是“定义业务战略和组织&#xff0c;关键业务…

源于加速,不止加速——10年沉淀,破局改变

20余年技术&#xff0c;面临破局。CDN(Content Delivery Network&#xff0c;内容分发网络) 是一个超大规模的分布式系统&#xff0c;为互联网各类App和Web站点提供动 / 静态内容、实时流媒体加速以及网络安全防护等能力。在线购物、直播、音乐、游戏、社交等等一切&#xff0c…

5分钟让你在大火的多模态领域权威榜单VQA上超越人类

ModelScope上开源了达摩院众多业界最强多模态模型&#xff0c;其中就有首超人类的多模态预训练视觉问答模型mPLUG&#xff0c;小编激动的搓搓小手&#xff0c;迫不及待的体验了一下。 一探&#xff1a;浅草才能没马蹄 市面上有好多号称“用户上手简单”&#xff0c;“一步到位…

私有化输出的服务网格我们是这样做的

介绍 微服务开发的问题 微服务架构下我们在开发中遇到的常见的问题有以下 4 个&#xff1a; 多语言问题&#xff1a;有多种编程语言&#xff0c;node.js, JAVA, GoLang…微服务需要为每种语言都维护一种中间件 SDK升级推动难&#xff1a;SDK 升级需要推动业务应用进行代码修…