单元测试的实践与思考
之前一直有一个想法:将测试过程的每个重要环节都进行拆解,然后详细说明这个环节重点要做的事情,为什么要做这些事,以及注意事项。
在星球群里和几位同学聊到了这个事情,有同学提议可否将单元测试环节加进来,斟酌一番,觉得还是很有必要的,就有了今天的这篇文章。
这篇文章,我会聊聊我对于单元测试的思考,以及些许实践。
软件研发测试的交付模型
软件从需求出现到最后的线上发布,大致要经历如下几个阶段:
广义上来说,在需求提出的时候,测试就需要介入开展相关的可测性评估。但狭义上来看,正式的测试活动开展(执行测试用例),一般是从单元测试环节开始的。
提到单元测试,大家可能首先想到的是单元测试框架,Sonar,测试的粒度要精确到方法、代码行甚至函数级别。
见过很多同学聊单元测试,都在疯狂追求代码覆盖率,好像代码覆盖率的指标越好看,最终的交付质量就一定会越好一样。但真的是这样吗?未必!
单元测试要解决什么问题
前面我写过单元测试的一些实践方法(见文章《测试需要做单元测试吗?》),这篇文章其实陷入了具体的技术细节中,即单元测试到底要怎么落地,落地执行的细节如何。
写下这篇文章的时候,我重新复盘了自己做测试工作以来在单元测试方面的实践过程,发现自己好像走进了一个技术的狭窄暗巷:太过于追求技术怎么实践落地,忽略了做单元测试的原因,以及做单元测试的背景。
从纯技术的角度出发,单元测试的粒度确实应该精确到每一个最小模块,即一个方法或函数。
但做测试工作的毕竟是软件工程师,从工程师的角度出发,在我看来单元测试应该考虑的是如何保证自己负责的部分(一个应用服务或者该应用服务中的某些功能模块甚至接口)达到质量要求。
简单来说,保证自己负责的部分技术设计和交付产物达标,也是单元测试的范畴。
回想一下,在开发阶段,研发同学都会进行本地自测,验证自己实现的功能是否符合预期。
很多时候由于上下游依赖的关系,没办法进行联调验证,因此为了避免外部影响,常见的解决思路就是直接return,或者mock(关于mock,可以参考我前面的文章《研发提效利器:聊聊mock服务化》)。
因为要保证自己负责部分达到质量要求,自己负责部分的最小粒度是由很多个方法函数构成,因此才有了单元测试框架、代码覆盖率等一系列事物的出现。
单纯追求单元测试覆盖率并不一定能提高质量,但不做单元测试,后期的测试活动开展要面临的风险和压力一定会上升。
一般在小公司,单元测试由研发自己负责,测试更多的是制定流程规范和提供用例,以及质量度量。当每次迭代单元测试覆盖率到达要求时,才会开展冒烟测试,确保整体交付到测试工程师手中的代码达到一定质量要求,满足可测性标准。
单元测试要解决的问题,就是确保每个阶段每个人负责的最小模块的质量,满足流转到下一环节的标准。
风险和缺陷越早发现修复,后面的发现和修复成本才会越来越低,这样才能整体上提高整个技术团队的交付效率和交付质量。
单元测试的实践注意事项
在具体的实践中,单元测试落地的最大挑战,主要有如下几项:
- 测试用例:应该确保先设计单元测试用例,再编写实现测试用例功能的代码,即TDD(测试驱动开发);
- 隔离依赖:执行单元测试时应确保被测单元的独立完整性,避免依赖项对结果的影响,常用的方法是mock;
- 测试数据:提前准备单元测试所需的数据,包括正常数据和异常数据,以充分验证代码在各种场景下的正确性;
- 测试顺序:应该根据调用关系合理安排测试用例的执行顺序,确保各单元之间的互相依赖关系得到正确的处理;
- 测试效率:落地单元测试之前就应该考虑将单元测试的执行纳入CICD流水线中,以便能够及时发现和修复问题;
- 异常处理:使用正确合理的断言来验证代码的执行结果是否符合预期,同时考虑错误场景和可能出现的异常情况;
- 维护成本:除了跟随迭代及时更新测试用例,还应该为测试用例和代码添加适当的文档注释,便于其他人理解维护;
最后,则是大家所熟知的测试覆盖率。提高覆盖率是为了尽可能多地覆盖代码中的语句、分支和条件,以确保代码的各个部分都得到了充分的测试。
但覆盖率指标仅可以作为一个参考值,辅助我们对单元测试执行的结果进行评估,而非一切唯指标论。