当我们在谈论单测时我们在谈论什么
关于如何提升团队的代码质量,我曾经做过很多尝试。由于团队成员都有繁忙的开发工作,公司也不是学校,不可能投入太多去面面俱到地教层层选拔招聘进来的程序员这些基础知识,所以,做法一般是以点带面,比如引入 sonar 代码检查推动大家去掌握一些以前未曾注意到的编码细节,比如通过针对性地培训降低代码的认知复杂度让大家掌握常用的重构技巧和设计方法。这些都能取得一定的效果,这次我想从自动化单元测试入手,更进一步……
什么是单元测试
教我的儿子 Allen 学习 Scratch/Python 和乐高是我周末最主要的一项“娱乐活动”,从孩子的视角去看待和学习编程语言和工程技术会给人很多意想不到的启发。正如上面的图片所示,有一次,Allen 在用乐高积木做一辆小车,当他把轮子组装好的时候,他会用手指拨弄一下,看轮子能不能转动。这时车子还远远没有成形……他竟然在做单元测试!!!
Allen 的这一个举动让我感到惊喜,我甚至开始幻想他未来一定会成为一个出色的工程师!他明白尽可能早,尽可能小地去做单测,这已经超过我工作中见过的 80% 的软件工程师了,在我看来,我所见过的大部分开发者做单测的时机都不够早,单元的粒度也不够小。
后来我看了一部名为《狼伴归途》的电影,这部电影让我意识到我陷入了“自己家的孩子,别人家的老婆,怎么看都觉得好”的思维误区里——Allen 明白的“尽可能早,尽可能小地做单测”的这个道理,我们的祖先在石器时代就已经明白了。
他们在制作长矛的时候,先会用石头打磨矛头,每磨一段时间,他们就会将即将成形的矛头在皮毛上划一下,验证矛头是否足够锋利,只有矛头合格了,才会给它装上一个长柄。
想想我们工作中整日忙碌,把自己还不太清楚是否足够锋利到能刺破动物皮毛的矛头和满是低级缺陷的粗糙的长柄迅速组装起来匆匆提测并交付给猎人的程序员们,被大家戏称为程序“猿”其实是实至名归——大部分人的技艺还处在非常原始的状态,还没有进入石器时代。
“尽可能早,尽可能小地做单测” 是我们这支人类赖以繁衍下来的最基本技能,它已经固化到 Allen 的基因里形成了固有行为模式。
为什么要做单元测试
孩子和原始人进行单元测试是基于一种本能或传统,他们不知道这背后的深层次原理。当我们提到测试的时候,不得不提到的是这个测试金字塔。
测试金字塔有多种的分层方式,上面这幅图引自《The Clean Coder》,不论哪种分层方式,其背后体现的经济学原理是一致的。
首先,越是在底层的测试类型,其测试的成本越低,反馈越及时。在单元测试阶段能发现的一个缺陷,假设修复它的成本是 10 块钱,那如果它没有在这个阶段被发现,而是推迟到了组件测试阶段,那修复它的成本是 100 元,以此类推,每晚一个阶段发现,其修复成本都会增加一个数量级——想一想那高昂的沟通和回归成本吧。如果在线上才发现这些缺陷,那成本和损失就更难以估量。
基于上面的原理,专业的开发团队应该选择一个合理的测试策略。
首先是测试覆盖率的要求不一样。单元测试的覆盖率终极目标可以追求 100%,但系统测试能做到 10% 就已经很昂贵了。
其次,是每种测试类型的关注内容应该不一样。单元测试关心代码层面的正确性,大多数的异常路径都是由单元测试来覆盖的,单元测试应该由开发者自己来做,组件测试更多关心成功路径的情况,以及一些明显的极端情况、边界状态和可选路径,组件测试可以由 QA 和业务人员来负责。
软件在其生命周期内会频繁地变更,这和建筑物、飞机汽车等非常不同,充分体现了软件”软“的一面。在这种高度变化的环境下,要每次做到这么高的测试覆盖率,成本是巨大的。幸好,也正是由于软件”软“的一面,自动化的测试在软件领域更容易实现。
什么是自动化单元测试
自动化单元测试 = 自动化 + 单元 + 测试
最近,我调研了一些自动化单元测试覆盖率是个位数的应用,下面用实例来说明什么不是自动化单元测试,然后大概就清楚了为什么对很多开发者来说自动化单元测试那么难。
个别 Java 开发者还在写 main 方法,通过 System.out.println() 的方式来做单元测试, main 方法很难被自动执行, println 的结果也需要人眼去盯着判断,显然这种单元测试不是自动化的。
大部分开发者懂得使用 JUnit,可惜很多人用 JUnit 的原因只是需要一个更好用的 main 方法而已,他们的测试代码里访问了数据库等有状态的外部资源,根本无法重复地孤立地执行,所以大部分工程在使用 maven 构建的时候都设置了 -Dmaven.test.skip=true。你没有看错,很多人用了 JUnit 这样的自动化测试框架,但却不想让它自动执行——就如点了宫保鸡丁但不要鸡丁一样,他们觉得宫保鸡丁里的花生米更好吃。是的,阉割了自动化执行的 JUnit 就只剩下花生米(一个更好的 main)了。显然,用了 JUnit,但并没有做自动化的单元测试。
我还遇到一些高级的开发者,他们不会犯上面这些低级的错误,他们甚至在自动化测试方面做了很多有价值的创新。比如,他们把线上的真实入参数据抓下来,变成 XML/JSON 数据,然后基于这些数据写单元测试。这种做法的一个主要问题是站的角度不对,这种测试是黑盒的,记得我们上一节说过吧,单元测试要覆盖大部分异常情况,抓再多的真实数据,也很难保证覆盖到大部分的异常,因为很多异常状况的发生概率本来就低。单元测试要覆盖大部分异常情况就必须有一部分是站在白盒的角度来写的。另外这种测试方式维护成本也很高,你仔细考量,它其实是测试金字塔里的组件测试。这种测试是有价值的,但更适合 QA 团队来负责。
以上种种都说明很多情况都做不到自动化,这是阻碍自动化单元测试落地的一个重要原因,但其实还有更深层的原因存在。
如何做好自动化单元测试
这个更深层次的原因就是单元,既然单元测试位于组件测试之下,那单元的粒度比组件还要更小。要做好单元测试,首要条件是要有单元。如果组件内的代码没有分成清晰独立的小单元,那单元测试就无从谈起。所以,三分测试,七分设计。
如果能将代码合理地拆分成不同的单元,你就会发现,大部分单元,如图中绿色部分所示,都是非常独立的,它们不依赖数据库等外部资源,只是一个内存的计算,所以这部分是非常容易做自动化单元测试的。
不好做单元测试往往是胶水单元和有外部依赖的单元。而这部分代码往往不是业务逻辑所在,代码结构也比较扁平,并不复杂。
所以,当你的应用的自动化单测覆盖率只是个位数时,先不要急着引入 MOCK 框架这类工具,当务之急是做这种单元化的改造,测试那些投入产出效果明显的部分。以后再用 MOCK 等方式测试其他部分。
诚然,做好单元测试有很多方法、技巧和工具,但首先我们会聚焦在这一点上。
最重要的事
你无法做好你不理解、不认可的事情。
这是我接触到的优秀的敏捷教练用行动告诉我的。在做敏捷实施的时候,首先要做的是培训敏捷的理念(你可以称之为"洗脑"),培训结束后,你的团队如果无法理解和接受敏捷的核心理念,优秀的敏捷教练一般都会告诉你,敏捷其实也要因地制宜,其实你们现在团队的做法就挺好的。
是的,如果你不认可这一篇文章所说的大部分理念,我想说的是,不做自动化单元测试也没什么,非常多成功的市值上千亿的互联网公司开发的软件都有着非常低的单测覆盖率。生命苦短,不要在单测这种小事儿上浪费时间。
作者:codeasy,志在分享哪些让编码变得简单的软件技艺——包括原则、技术、工具和实践。
声明:本文为作者投稿,版权归对方所有。
热 文 推 荐
☞ 和 Eclipse 并肩十年后,我终于「投敌」IDEA 了
☞ 下一次 IT 变革:边缘计算(Edge computing)
☞ 年度重磅:《AI聚变:2018年优秀AI应用案例TOP 20》正式发布
print_r('点个好看吧!');
var_dump('点个好看吧!');
NSLog(@"点个好看吧!");
System.out.println("点个好看吧!");
console.log("点个好看吧!");
print("点个好看吧!");
printf("点个好看吧!\n");
cout << "点个好看吧!" << endl;
Console.WriteLine("点个好看吧!");
fmt.Println("点个好看吧!");
Response.Write("点个好看吧!");
alert("点个好看吧!")
echo "点个好看吧!"