写有价值的单元测试

发表于:2017-9-22 17:04

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:ali家鸽    来源:51Testing软件测试网采编

  单测要有强度
  有些同学写的测试里面会有Assert,但用的很少,往往只是在最后用一个assertNotNull(result),这样的测试强度是不够的。举个例子,假设有以下的待测方法
  图1
  以下的测试用例强度就太差了,这个用例虽然也用了Assert,但对测试的结果校验很弱,即没有校验结果中有多少User,也没有校验双向模糊逻辑是否正确实现了。实际上即使查询结果是空,返回的也是个empty list,测试用例还是不会报错。
  图2
  单测要能反应函数的明确需求才算有强度。这样以后 函数的实现一旦被改错了单测才能尽快报错, 针对以上这个例子,单测至少要达到以下强度。
  图3
  单测要有覆盖度
  强度是指单元测试中对结果的验证要全面,覆盖度则是指测试用例本身的设计要覆盖被测试程序(SUT, Sysem Under Test)尽可能多的逻辑。只有覆盖度和强度都比较高才能较好的实现单测的目的。
  按照测试理论,SUT的覆盖度分为方法覆盖度,行覆盖度,分支覆盖度和组合分支覆盖度几类。不同的系统对单测覆盖度的要求不尽相同,但这是有底线的。一般来说,程序配套的单测至少要达到>80%的方法覆盖以及>60%的行覆盖,才能起到"看门狗"的作用,也才是有维护价值的单测。
  等价类划分可以帮助我们用更少的测试代码写出更高覆盖度的单测。单元测试是典型的白盒测试,等价类的划分以及单元测试的编写最好都由SUT的编写者自己去完成,这样整体效率最高。
  单测粒度要小
  和集成测试不同,单元测试的粒度一定要小,只有粒度小才能在出错时尽快定位到出错的地点。单测的粒度最大是类,一般是方法。单测不负责检查跨类或者跨系统的交互逻辑, 那都是集成测试的范围。
  通俗的说,程序员写单测的目的是"擦好自己的屁股",把自己的代码从实现中隔离出来,在集成测试前先保证自己的代码没有逻辑问题。至于集成测试乃至其它测试中暴露出来的接口理解不一致或者性能问题,那都不在单元测试的范围内。
  单测要稳定
  单元测试通常会被放到持续集成(CI)中,每次有代码check in时单元测试都会被执行。如果单测依赖有对外部环境(网络、服务、中间件)的依赖,任何一次网络抖动或者返回的变化都会造成单测失败进而造成持续集成的失败。这会造成整个持续集成有大量误报,进而导致持续集成机制的不可用。所以单测不能受到外界环境的影响。
  为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring或者guice这样的DI框架注入一个本地(内存)实现或者Mock实现。用这种方法保证在SUT出错时单测才会报错,持续集成才能更稳定,单测的失败也才更重要。
  单测速度要快
  作为"看门狗",最好是在每次代码有修改时都运行单元测试,这样才能尽快的发现问题。这就要求单元测试的运行一定要快。一般要求单个测试的运行时间不超过3秒, 而整个项目的单测时间控制在3分钟之内,这样才能在持续集成中尽快暴露问题。
  单测不仅仅是给持续集成跑的,跑测试更多的是程序员本身,单测速度和程序员跑单测的意愿成反比,如果单测只要5秒,程序员会经常跑单测,去享受一下全绿灯的满足感,可如果单测要跑5分钟,能在提交前跑一下单测就不错了。
  实际上,上一条要求将单测的外部依赖全部改成本地实现或者Mock,除了系统稳定性外,执行速度也是考量之一。改成本地实现或者Mock后,绝大多数单测运行的时间都非常快,基本上可以说是瞬间就能跑完。
  单元测试的方案
  明确了单测的目标之后,单测方案的选型也比较明确了。原则就是本地化和快**,选型上也尽量以内存方案为主。
  下面我们以Spring boot开发为例,给出一套解决方案,以下代码都是以Spring Annotation Configuration给出的,如果有必要也可以换成XML
  数据库测试
  数据库测试多用在DAO中,DAO对数据库的操作依赖于mybatis的sql mapper 文件,这些sql mapper多是手工写的,在单测中验证所有sql mapper的正确性非常重要,在DAO层有足够的覆盖度和强度后,Service层的单测才能仅仅关注自身的业务逻辑。
  为了验证sql mapper,我们需要一个能实际运行的数据库。为了提高速度和减少依赖,可以使用内存数据库。内存数据库和目标数据库(MySQL,TDDL)在具体函数上有微小差别,不过只要使用标准的SQL 92,两者都是兼容的。下面的方案中就使用H2作为单测数据库。
  数据库单测方案需要解决3个问题:
  1. Schema的初始化和同步
  2. 每个测试完成后的数据清除
  3. 调试过程查看数据库内容
  下面的方案中省略了对这3个问题都给出了方法
  每个公司和应用的不同,所用方法有差异,具体问题具体分析。
  单测的误区
  以上介绍了单测的理念和方法,下面介绍一些通常对单测的理解误区。
  补单测
  经常听到开发汇报说"XXX功能已经写完了,今天的工作是补些单测"。愿意补单测是很有责任心的表现,但还是要说单测应该随着代码同时产生,而不应该是补出来的。
  按照错误率恒定定律,错误的产生是客观存在的。一次性手写超过20行代码基本就会出错。当一段代码(一个类或者一个方法)刚被写出来的时候,开发对整个上下文非常清楚,要测试什么逻辑也很明确(再次强调单测是白盒测试),这时候写单测速度最快,也最容易设计出高强度的单元测试。如果等一次产出N个类,上千行代码再去写单测,很多当时的上下文都已经遗忘了,而且惰性会使人面对大量工作时产生畏难情绪,这时写的单测质量就比较差了。至于为几个月甚至几年前的代码写单测,基本上除了大规模重构,是没人愿意去写的。
  在测试前置这方面最激进的尝试是TDD (Test Driven Development),其次是TFD (Test First Development),它们都要求单测在代码前完成。尽管这两个实践目前不是很流行,但还是推荐有兴趣的同学去尝试一下TDD,经过TDD熏陶的代码会自然的觉得单元测试是程序的一部分,对于这点理解也会更深。
  项目紧,没时间写单测
  这也是开发经常会说的话,尤其是没有写单测习惯的开发经常会说的话。然而这句话其实也是不对的,不考虑单测框架自身的学习成本,任何情况下写单测都只会降低整体交付时间
  根据"错误率恒定定律"和"规模代价平方定律",因为单测可以在尽量小规模内发现问题,其实这是一个很自然的结论。再紧的项目都要有设计、编码、测试和发布这些环节,如果说项目紧不写单测,看起来编码阶段省了一些时间,但必然会在测试和线上花掉成倍甚至更多的时间来修复。
  错误率是恒定的,需要的调试量也是固定的,用测试甚至线上环境调试并不能降低调试的量,只会降低调试效率。
  单测是QA的工作
  这是混淆了单元测试和集成测试的边界。
  单元测试是白盒测试,应该随着代码一起产出,一起修改。单元测试的目的是让程序员"擦干净自己的屁股",保证相对小的模块确实在按照设计目标工作。单元测试需要代码和程序同时变动,不要说QA,就是换个开发写单测都赶不上这个节奏(除非结对编程)。所以单元测试一定是开发的工作。
  集成测试是黑盒测试,一般是端到端的测试,很大的工作量在维护上下游环境的兼容上。集成测试运行的频率也比单元测试低,这部分工作由QA来作还是可以接受的。
  总结
  越是重要的项目,程序员越需要安全感。单元测试就是程序员的救生圈,在代码的海洋中为程序员提供安全感。有了单元测试的保障,程序员才有信心在约定时间内完成联调和发布,才敢对已有的程序作修改和重构而不担心引入新问题。
  作为软件开发中投入产出比最高的实践,我们要更大力度的推广单元测试。让更多的程序员尝到它的好处,从而爱上它。
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号