目录

杂谈单元测试的价值、困境、误区、原则、指南与原子正确性。

单元测试概念

具体来说单元测试到底是什么?

单元测试是一种软件测试方法,通过编写代码来验证应用程序中最小可测试单元(如单个函数、方法或类)的正确性。通常,单元测试由开发人员在功能实现过程中或完成后编写,其目的是确保每个最小可测试单元都能按照设计预期正常工作。

单元测试关键概念:

  • 独立性。不依赖其他测试或外部资源。
  • 自动化。开发过程中会频繁执行,目前自动执行检测。
  • 可重复性。可重复,且每次运行结果应该一致,除非代码有改动。
  • 覆盖率。单测的一个目标是尽可能的覆盖代码路径。
  • 断言。单测需要有明确的断言。没有断言的单测是伪单测。

单元测试的价值与必要性

我们都知道单元测试的重要性,那单元测试有哪些价值呢?

  • 提高代码质量。

    质量涵盖正确性、可靠性、稳定性、可维护性。甚至单测可以反向要求代码满足设计原则,比如开闭、单一职责、里氏替换、依赖倒置、合成复用、接口隔离、迪米特原则。

  • 提高效率。

    看似增加了开发成本,但在开发期间就能节省大量调试时间,且长期来看,减少了回归成本,我们也会尽量去减少单测成本。

  • 便于重构和维护。

    单元测试会迫使代码做更好的分层,且代码原子化缩小影响范围,便于重构维护。

  • 样例使用手册。

    有助于他人及自己更具体地理解代码与预期行为,而不需要带着抽象的代码在脑海里想象。

  • 增强信心。

    单测有助于开发人员更自信地进行代码修改和重构。

原子正确性与单元测试

一个函数或方法肯定没有问题,也就是函数提供的能力正如函数签名(包含入参与出参)说表达的,这种能力,我们可以称为原子正确性。

函数/方法形如细胞,相同的细胞组合成组织,不同的组织构成成器官,器官组合成生命体。单测的目标是确保重要细胞是健康的,从下往上可以正确健康地组合成组织、器官,直至生命体。

单测可以让问题反馈路径缩短,从 AI 辅助编码->人工确认->修改代码->运行单测->确认结果->发现问题->AI 辅助编码修复,周而复始,都在本地开发阶段完成。

避免将问题,比如边界、异常、兼容性等遗留到测试阶段,带来 bug 记录、复现、排查分析、沟通等成本。

这篇文章关于原子正确性与单测讲述的很好:一定要写单元测试!

哪些代码需要单元测试

代码是否有分类,不论新旧项目,是否能从单元测试收益与成本角度初略划分出代码类别?

  • 算法类代码。

    大部分属于重要基石代码,与业务逻辑耦合度低,扇入大,即模块化且可复用性高。单测收益高,且由于代码原子性与稳定性,单测成本低。

  • 业务调度类代码。

    与业务逻辑耦合度高,扇入小,即模块化与可复用性低。单测收益低,且由于代码复杂性,单测成本高。但适合进行集成与接口测试。

  • 琐碎代码。

    衔接代码或零散一两行代码。不需要考虑单测。

  • 复杂代码。

    可能是历史技术债务,可能包含大量兼容性代码,可能耦合性高直接依赖外部因素。估计都不好写单测,反而需要重构才能写好单测,但重构的成本更高,收益低。什么时候考虑重构,再根据实际情况考虑。

    依赖程度越高,测试成本越大。

单元测试面临的困难

对于一些语言和环境来说单元测试存在一些门槛,比如:

  • 环境与配置复杂,门槛高,记不住,不稳定。目标:开箱即用。
  • 测试用例选择困难。目标:善用 AI 辅助生成测试用例。
  • 单元依赖外部因素。目标:善用 stub、mock 解除依赖。
  • 回报不足。目标:认识到单测的投入可以尽量避免测试阶段陷入 bug 的狂轰乱炸与慌乱状态,以及仓促修复导致代码资源组织不善。

遗留代码及其优化解决方式

  • 较老的技术栈或框架

    • 数据库老旧。
    • 开发语言版本低。
    • 框架引擎版本低。
  • 兼容老旧功能的代码

    • 不好分辨是否要兼容。
  • 缺乏文档与单元测试的代码

    • 接口代码与文档割裂,未能自动化。
    • 单元测试缺失,单元测试边界不清。
  • 优化解决遗留代码的方式

    • 推翻重来。人力和时间不允许。如果允许的话考虑愚公移山的方式。
    • 代码重构。在每次迭代里,可以基于较为模块化的代码进行重构,避免遗漏。
    • 补充单元测试。新的迭代中一旦涉及到旧方法函数,考虑对其补充单元测试。对于旧项目中新增模块化代码,必须选择性单测。

单元测试误区

  • 1、缺乏断言的假单测。
  • 2、单测作为白盒测试,应该视为对方法函数签名的黑盒测试。
  • 3、未解除外部上下游的依赖的单测。应该善用 mock。
  • 4、单测粒度过大。单测并不是对接口或是一个业务流程的单测,单测是对最小可测试单元进行测试。一个个可靠的单元从下而上构建了一个可靠的整体。
  • 5、盲目所有代码块单测。单测不需要对所有代码进行测试,选择性单测,单测有收益的同时也有成本,衡量收益与成本。对模块化的算法类、上层数据与流程服务调度协调代码重点单测。
  • 6、流水式面条式的历史复杂代码不适合单测,优先考虑拆解重构,单测的前提做好代码分层与模块化实现。

单元测试遵循的原则

  • 独立性。要求单测颗粒度适当,满足独立性。
  • 可重复执行。不依赖于外部因素,满足可重复执行。
  • 自动化。可自动化执行。
  • 有明确的断言。
  • 执行速度快。
  • 分支等价与边界测试充分。
  • 覆盖率高。
  • 相同语言尽量采用相同的单元测试规范。
  • 前期应用层面代码,选择性单测。

覆盖率

一般我们在准备用例时,会关心比如分支覆盖、条件覆盖等,单测完成后可以生成对应的覆盖率报告。覆盖率高才能尽可能地保证单测代码质量。

对于复杂逻辑或关键路径,也可以考虑手动检查覆盖情况。

单元测试一些基础指南

AI 实时辅助编程,85% 以上的代码都可以由 AI 工具辅助完成,工程师要做的是:任务分解、(子)任务描述、AI 工具提示词与规则设定、代码校验。

虽然 AI 工具可以也可以完成单元测试的大部分代码,也需要工程师协助补充说明用例及单测代码校验。所以我们也需要熟悉单测的一些技巧与编写方式,才能校验 AI 工具辅助的单测

  • go 单测使用指南.md
  • go 单测基准测试.md
  • go 单测指令与参数.md(虽然 IDE 一般都隐藏在按钮下,对于实际运行指令与参数也要有个底)
  • phpunit 基础使用指南.md
  • phpunit 指令与参数参考.md
  • phpunit.xml 配置说明.md
  • phpunit 测试套件指南.md
  • phpunit 与项目框架衔接指南.md
  • Mockery 模拟框架指南.md

9ong@TsingChan 文章内容由 AI 辅助生成。