七的博客

理解单元测试的价值以及局限

开发经验

理解单元测试的价值以及局限

说起单元测试,这是一个软件开发过程中经常被讨论的话题。很多程序员可能都是知道这个概念,但是在实际的项目中几乎比较少去写,就算写很多时候也是为了应付公司的代码检查流程。

我将总结下我在项目中实践单元测试的一些经验。

1. 不写单元测试的原因

为什么不写测试用例的原因,每家公司基本都出奇的一致,基本都是下面几点:

  • 项目周期比较紧,优先完成功能的开发才是首要任务,快速上线才是王道。在代码质量以及项目进度中,大部分公司更加注重项目进度,对于代码质量相对更没那么在意。

  • 缺乏单元测试编写经验。在国内的软件开发人员很多,但是有很大一部分开发人员没有去了解过单元测试。所以如果直接上手写单元测试很可能不知道该如何开始。

  • 编写测试用例非常花时间。有时候编写业务功能代码可能只需要 50% 的时间,但是编写测试用例可能会比这个时间更长。短期来看,越是复杂的功能编写单元测试就会越延长开发周期。

  • 很多项目的生命周期就几个月,过了这几个月可能软件不会继续迭代维护。这种情况下,花时间去编写单元测试没有任何意义。

  • 缺乏一定的激励措施。 单元测试写的好,很多公司不会有额外的奖励措施。让软件正常运行、尽量没有 bug 是一个开发的基本技能,所以公司不太可能会针对这个有奖励措施。

  • 维护成本高。业务逻辑一改变,测试用例可能就要大幅度进行调整,甚至很多时候测试用例改的跟重写差不多。

2. 单元测试的收益

在很多公司的项目中,都是有编写单元测试的必要,特别是一些参与人员多的大项目里面。

编写单元测试比较明显的收益:

  • 提高代码质量。通过单元测试,可以从小的细节方面去测试函数,避免漏掉一些开发过程中没有考虑到的分支判断。

  • 为后续的重构提供保障。有了单元测试的覆盖,后期项目如果需要重构的话会轻松许多,同时也可以大胆的重构然后进行测试验证。

  • 提升开发效率。每次项目代码做改动时,只要测试用例全部通过,基本可以保证不会有明显的逻辑 bug。并且通过自动化的测试流程,可以快速的暴露出 bug,开发人员可以更快的改进代码。

  • 测试用例可以提供一些文档的作用。 对于刚接手项目的新人来说,通过项目里面的单元测试用例可以快速的了解代码逻辑。比起一些更新不及时的文档来说,这种【灵活的文档】更受开发欢迎。

  • 更早的发现 bug,降低修复 bug 的时间。 在测试人员测试之前,开发通过单元测试可以提前发现一些隐藏的 bug。 功能提交到测试团队测试,再到测试团队反馈问题,最后到开发修复 bug , 这几个流程都需要很多时间。 但是如果再开发阶段就减少这种 bug , 那么整个一个项目开发流程就可以得到加速。

3. 不适合编写单元测试的项目场景

上面说的一些编写单元测试的收益,也是需要在合适的场景中才会体现出来,并不是所有的项目都适合投入精力去写单元测试。

有些项目场景确实是不太适合的,就不用强行编写:

  • 外包类的项目,这种项目通常交付出去不会有后续的迭代维护,写测试用例不太划算。

  • 概念性验证的项目。比如早期做团购的时候,很多互联网公司为了抢占团购市场,都会要求快速迭代上线验证市场。 这种项目主要是为了验证某个领域或者新赛道的项目是否有前景,所以会频繁更改需求,代码相对也不稳当。编写单元测试只能拖慢开发进度,错过了风口了这种项目就没有价值。

  • 科研类的项目。这种项目的目的很明确,就是为了科学研究和学术目的。比如学校、xx科学院、xx研究所的一部分项目就是这种类型,这些项目完成的时候就已经完成了它的使命,一般不需要去编写单元测试。

  • 简单的增删改查项目。 这种项目大部分几乎没有很多逻辑的项目,这种项目写单元测试的意义不大。

  • 公司遗留的老系统。比如公司有这种运行了 10 几年的内部管理系统,这种系统的问题就是使用了很多过时的技术栈,代码高度耦合等。这种系统如果还具备价值,要么就是会一直用现有的稳定版本,要么就会考虑重构整个系统。为这样的老系统代码编写单元测试的性价比很低,同时成本过高。

4. 适合编写单元测试的项目场景

比较适合写单元测试的项目:

  • 长期迭代维护的核心业务系统项目。这种项目长期维护更新,通常业务逻辑会越来越复杂。同时在迭代的过程中经历多批开发人员接手,如果有单元测试可以保证代码的质量跟稳定性,确保每个小的改动不会影响系统的稳定性。

  • 参与项目的人数比较多,并且人员技术水平参差不齐。因为参与开发的人数多,通常这种项目会有比较复杂的依赖关系。通过单元测试可以确保每一个模块可以独立工作,减少集成的时候一些问题的处理时间。

  • 项目业务逻辑比较复杂的项目。 这种项目通常会有很多的条件判断、数值计算、状态变化等逻辑,为这种逻辑编写大量的单元测试收益就会很高。如果是通过集成测试或者是端到端的测试,花费的时间就会特别的多,而且刚还不容易发现问题。

  • 涉及到金钱类的项目。这种跟钱打交道的项目,错误成本比较高,如果因为一个小 bug 导致多扣钱或者少扣钱,对公司或者客户来说都是一个损失。同时一旦出现了明显的 bug ,就容易收到用户的质疑,然后放弃使用。同时这种项目出现 bug 还容易受到监管部门的注意,面领着罚款以及法律诉讼的风险。

  • 公共的库或者框架。 这种通常是公司内部的基础设施团队进行提供,整个公司或者集团内部要进行推广使用。 所以务必要保证在各种场景下运行都符合预期,全面的单元测试就很有必要。

  • 开源的项目。 开源的项目通常会有多个贡献者,这种项目编写单元测试可以帮助维护代码的质量。同时高覆盖率的测试用例也可以让使用的人更加放心,毕竟谁也不想用一个三天两头出 bug 的开源项目。

5. 单元测试的局限

尽管单元测试看起来,会给项目产生一定的收益,但实际上单元测试也存在很多的局限性。

  • 覆盖率的限制。 通过单元测试尽管可以覆盖大部分的代码逻辑,但是很难保证持续 100% 的覆盖率。在我以往的项目中,有些复杂逻辑很难完全通过单元测试进行验证。只有拆分的足够细才能更好的进行测试,但是这样会将类拆分的很多,对代码的维护也是一个挑战。

  • 依赖外部的一些接口或者系统。 单元测试通常只测试代码本身,不会涉及到外部的一些接口或者系统,比如常见的就是数据库、发起网络调用等。即使我们使用一些 Mock 组件将外部依赖屏蔽掉,但是始终 Mock 的不会特别完善,一些异常场景没有办法很好的被 Mock 掉。 不过这已经涉及到集成测试的范畴了,集成测试但是可以测出来这些问题,就是花费的时间长,而且搭建环境麻烦。

  • 维护成本。 随着代码的更新迭代,单元测试也就需要不断地更新跟维护。 如果是自己写的单元测试用例那还好一点,毕竟自己的代码自己最熟悉。 如果是别人编写的单元测试代码,要修改很多地方甚至还要去查看逻辑,那么就会有抵触情绪。 同时如果测试代码质量不高的话,或者写的不够灵活,每次修改这些测试用例都可能要花费很多的精力。 这样就有点得不偿失,演变到最后可能会直接把不通过的测试用例给屏蔽掉。

  • 虚假的安全感。 虽然一直在说单元测试的价值怎么样,但是单元测试显然并不能完全检测出所有的错误。 过份的依赖单元测试会让开发有虚假的安全感,因为很多时候开发人头自己编写的测试用例很多时候是顺着开发思路去写的,所以有些问题是不会暴露出来的。 正如每次别人怀疑自己代码有问题的时候,是不是就会脑子里先蹦出一句【你操作不对吧,我的代码怎么会有问题】。这样情况下,还是得辅助集成测试或者端到端的测试,来完善这些单元测试中没有发现的 bug。

5. 最佳实践

我们可以充分发挥单元测试的一个优势,同时规避它的局限性。下面总结一些项目中的单元测试实践经验:

  • 提前预估短期项目是否会变成临时项目,如果确定了是短期项目,那么可以直接跳过单元测试。 如果可能会变成长期项目,那么可以针对性的对一些复杂的逻辑编写一部分单元测试。 在不影响项目进度的情况下,适当的编写一部分可以。

  • 从短期来看,编写单元测试确实是会延长开发周期的。 但是从长远来看,比较高的测试用例覆盖会节省很多的调试以及修复 bug 的时间。

  • 可以优先为核心功能跟容易出错的部分逻辑编写单元测试,盲目的追求 100% 的覆盖率不划算。很多时候只需要覆盖 80% - 90% 左右收益是最高的,为了额外的 10% - 20% 花很多时间不值得。

  • 编写更加高质量的测试代码。测试代码应该简洁明了,通过 Arrange (准备数据) > Act (执行)> Assert (验证) 这样的顺序进行测试代码的编写。这样编写出来的测试用例代码变得更加容易阅读,在后期维护的时候可以降低很多不必要的麻烦。

  • 业务代码尽量的保持整洁。结构合理的函数可以快速的写出相对应的测试用例,而不是使劲去了解它的一些具体的实现细节。同时注意将复杂的功能拆分成多个简单的、独立的小模块或者小函数,这样写单元测试会简单很多,而且更容易覆盖各种情况。