什么是单元测试
引用维基百科:
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试 ,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
为什么要用单元测试
背景
- 软件可靠性,安全性、稳定性很重要,尤其是在航天软件、嵌入式软件中这些因素比在其他类型的软件中更为重要,编码过程中的一些小失误极有可能造成很大的损失,所以需要严格保障软件质量。
- 系统集成测试、功能测试等黑盒测试手段无法覆盖所有的代码逻辑与代码分支。还有很重要的一点就是系统级别的测试就算发现了bug,想要定位bug也需要花费很大的精力。
- 在研发阶段发现并修复bug的成本是比后期(如系统测试阶段、软件部署阶段、软件运营阶段)低很多的,所以要尽量在早期阶段发现并修复bug。
单元测试的作用
单元测试可以完全覆盖所有代码逻辑与代码分支、在早期编码阶段就能发现软件bug、在早期研发阶段可大大提高研发人员的编码与调试效率、代码修改后做回归测试、向研发人员澄清软件需求、协助设计具有良好可集成性的代码。
单元测试可大大提高代码调试效率
- 高覆盖率的单元测试,可以有效防止陷入耗时和让人手足无措的代码调试中,可以大大降低使用调试器调试问题的时间。调试器可以用来分析细微的问题,或者调试失败的单元测试,但调试器不应该是保障代码质量的工具
单元测试可以很容易检查回归测试后的代码
- 每次更改完代码后,只要运行一遍单元测试就能知道本次修改有没有带来额外的问题
从澄清软件需求和后续代码集成的角度看,单元测试也是有极大帮助的
- 良好的单元测试设计可以直接向研发人员澄清软件需求。单元测试是可被执行的产品文档,因为单元测试精确地展示了代码是如何被设计与使用的。可以说单元测试是一组非常有用的代码示例
- 可以被单元测试的代码,一定也具有良好的集成性。单元测试可以促进实现整洁且良好的接口,可测试性实际也是良好的可用性设计,也就是说,如果一段代码可以很方便的与测试夹具集成,那么这段代码也可以很容易的集成到产品代码中去。
单元测试的必要性
在很多软件项目的早期阶段大家会认为单元测试是一项劳民伤财的做法,投入与产出不成正比,编写单元测试看起来像一项毫无意义的付出。但当系统代码达到了一定规模后,研发人员会花费相当的精力去调试代码(其实调试代码所花费的精力在整个代码构建过程中占据了很大比例),可能很多问题都是比较难以调试出结果的,这会是一个令人沮丧的工作过程,我们会陷入重复无休止的单步调试过程,在单步模式中一遍又一遍的调试代码,还需要分析各种变量的变化。这非常浪费时间且降低开发速率。
在软件项目开发过程中以及项目中后期,单元测试就会显示出其积极的作用:
- 单元测试可以覆盖所有的代码逻辑与代码分支,通过单元测试研发人员在编码过程中通过运行单元测试代码可以很及时的发现自己代码中的bug,用单元测试调试代码比用调试工具更高效且有效。在TDD开发模式中,需要先搭建功能代码测试的手脚架(单元测试),然后再进行编码,研发人员每写一个函数、一个类都可以通过运行单元测试代码来验证自己代码的正确性。
- 在软件交付后,如果我们修改了代码,可以很快速的通过单元测试来做回归测试,单元测试可以快速给出整个代码项目工程的即时反馈,研发人员在在短短几秒钟就知道代码库中的代码是否能正常运行
- 通过阅读单元测试代码,研发人员也可以很方便地理解别人写的代码
- 单元测试可以帮助我们避免繁琐、无休止的调试。有趣的是调试主要用于分析错误现象不直观的失败的单元测试
QA可以替代研发人员自测吗
即使有一个单独的QA团队来测试软件,研发人员的目标应该是QA没有发现任何缺陷。
将一个已知的有缺陷的软件移交给QA团队测试是非常不专业的行为,专业的研发人员永远不会把保障系统质量的责任交给QA团队,QA只是我们安全体系中的第二道防线。
怎么用单元测试
编写单元测试的基本原则
- 证单元测试代码和产品功能代码一样重要,它不是“二等公民”,它需要被思考被设计被照料,它应该像产品功能代码一样保持整洁
- 整洁的测试代码有三个要素:可读性、可读性、可读性,可读性在单元测试中甚至比产品功能代码中更加重要。良好的单元测试代码可以帮助我们更好的理解产品功能代码
- 单元测试代码也是会随着产品代码的演进而变化,单元测试代码越肮脏就越难以修改,我们进行单元测试的代价就会变高,也无法确保单元测试代码的质量与准确性
- 单元测试代码的命名要直观容易理解,如果单元测试失败,研发人员可以迅速知道谁的单元测试失败了、单元测试测了什么、单元测试的环境是啥。
- 直观且容易理解的单元测试名字包含以下几点,例:cacheIsEmpty_addElement_sizeIsOne
- 单元测试的前置条件
- 单元测试的测试部分,通常是被测的函数
- 单元测试的预期结果
- 另一种建立直观且容易理解的单元测试名称的方法,就是在单元测试名称中显示特定的需求,单元测试的名称能反映应用程序的需求,例:creatingNewAccountWithExistingEmailAddressThrowsException
- 直观且容易理解的单元测试名字包含以下几点,例:cacheIsEmpty_addElement_sizeIsOne
- 单元测试的独立性。每个单元测试和其他单元测试都是必须是完全独立的,某个单元测试的执行结果与执行过程不应影响其他任何单元测试,如果单元测试之间是以特定的顺序执行的,这将是致命的。
- 单元测试环境的独立初始化。在一个干净整洁的单元测试运行结束后,与该单元测试相关的状态必须消失。每个单元测试都必须是应用程序的一个独立的可运行的实例,每个单元测试都必须完全自行设置和初始化其所需的环境,执行完毕后也要清理环境
- 不需要对第三方库进行单元测试,我们默认第三方库的可靠性,在自己的软件项目中也绝不要用质量没保障的第三方项目
- 一个测试用例一个断言
- 如果用例执行失败,研发人员可以快速找到失败原因
- 如果一个用例中有多个断言,前面一个断言执行失败会打断其他测试的执行
- 测试用例的命名要精确、易懂,要符合单一职责原则
- 不要对第三方系统做单元测试,这不是我们的责任
- 单元测试最好要完全运行在计算机内存中,尽量不要访问数据库、磁盘文件,因为这样会降低单元测试用例的执行速度。单元测试的执行必须要快速,否则当单元测试的数量增多后会拉低研发人员的效率
整洁的单元测试例程
摘自《clean code》,做了些许调整。该用例清晰的分为这些环节:构造-操作-检验-清理
// 测试用例名称一定要精确、易懂(本例是以需求命名的)
public void testGetPageHierarchyAsXml
{
// 构造环境
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
// 操作被测函数
submitRequest("root", "type:pages");
// 检查结果
assertResponseIsXml();
assertResponseContains("PageOne", "PageOne.ChildOne", "PageTwo");
// 恢复环境
recover();
}