规范与重构02:单元测试与代码的可测试性

一、代码重构的技术手段:单元测试

1. 什么是单元测试?

单元测试是开发自己编写的用于测试自己代码正确性的代码。相比于集成测试,单元测试更注重类和函数的逻辑是否按照预期执行了,是属于代码层级的测试。

2. 为什么要写单元测试?

  • 发现你代码中的bug
  • 发现代码设计上的问题,如果单元测试写起来很吃力,那么极有可能代码设计的是不够合理的
  • 对集成测试的有力补充,尤其是一些边界条件的测试
  • 写单元测试的过程本身就是代码重构的过程
  • 阅读单元测试能够帮你快速熟悉代码,实际上单元测试就是用户用例,反映了代码的功能和如何使用
  • 单元测试是TDD可落地执行的改进方案(测试驱动开发)
  • ....

3. 如何编写单元测试?

这里总结出了一些经验:

(1)写单元测试真的很耗时吗?

确实,过程很繁琐,但是基本都是cv。

(2)对单元测试的代码质量有什么要求吗?

不会产线运行,也不会相互依赖,可以放低要求。

(3)单元测试只要覆盖率高就够了吗?

60到70即可,不必过度追求单元测试覆盖率。

(4)写单元测试需要了解代码的实现逻辑吗?

要着重关心被测函数实现的功能而非内部逻辑。

(5)如何选择单元测试框架?

不需要太复杂的技术,大部分单元测试框架都能满足。

4. 如何在团队中推行单元测试?

首先,100%落实执行单元测试是一件“知易行难”的事。

很多历史代码因为没有单元测试而难以推行,需要每个开发都要有主人翁意识。

此外,程序员应该是智力密集型行业,但是目前很多都是劳动密集型,既没有单元测试,也没有Code Review。写好代码直接提交,然后交给黑盒测试,然后再改bug,如此反复。但实际上,在完善单元测试的情况下,可以很大的减少黑盒测试的投入。

关于单元测试的更多详细信息,可以参考之前的博客中关于单元测试我在团队中的一些实践。

二、代码的可测试性

1. 什么是代码的可测试性?

粗略地讲,所谓代码的可测试性,就是针对代码编写单元测试的难易程度。对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很费劲,需要依靠单元测试框架中很高级的特性,那往往就意味着代码设计得不够合理,代码的可测试性不好。

2. 编写可测试性代码的最有效手段

依赖注入是编写可测试性代码的最有效手段。通过依赖注入,我们在编写单元测试的时候,可以通过 mock 的方法解依赖外部服务,这也是我们在编写单元测试的过程中最有技术挑战的地方。

3. 常见的 Anti-Patterns

反面模式(anti-pattern或antipattern)指的是在实践中经常出现但又低效或是有待优化的设计模式。

常见的测试不友好的代码有下面这 5 种:

  • 代码中包含未决行为逻辑
  • 滥用可变全局变量
  • 滥用静态方法
  • 使用复杂的继承关系
  • 高度耦合的代码

三、如何通过封装、抽象、模块化、中间层等解耦代码?

前面讲到,重构可以分为大规模高层重构和小规模低层次重构。大型重构是对系统、模块、代码结构、类之间关系等顶层代码设计进行的重构。对于大型重构来说,最有效的一个手段莫过于“解耦”。

下面分三个部分详细说一下这个“解耦”:

1. “解耦”为何如此重要?

如果说重构是保证代码质量不至于腐化到无可救药地步的有效手段,那么利用解耦的方法对代码重构,就是保证代码不至于复杂到无法控制的有效手段。

“高内聚、松耦合”是一个比较通用的设计思想,不仅可以知道细粒度的类和类之间关系的设计,还能指导粗粒度的系统、架构、模块的设计。相对于编码规范,它能够在更高层次上提高代码的可读性和可维护性。“高内聚、松耦合”的特性可以让我们在阅读和修改代码的时候专注在某一模块,修改代码不会牵一发而动全身。

除此之外,代码“高内聚、松耦合”,也就意味着,代码结构清晰、分层和模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。即便某个具体的类或者模块设计得不怎么合理,代码质量不怎么高,影响的范围是非常有限的。我们可以聚焦于这个模块或者类,做相应的小型重构。而相对于代码结构的调整,这种改动范围比较集中的小型重构的难度就容易多了。

2. 代码是否需要“解耦”?

判断这个问题的标准有很多,比如说看看这个代码修改后的影响范围,看代码修改会不会牵一发而动全身。此外,还有一个直接的衡量标准,那就是把模块与模块之间、类与类之间的依赖关系设计出来,根据关系图的复杂性来判断是否需要解耦。

3. 如何给代码“解耦”?

(1)封装与抽象

封装和抽象作为两个非常通用的设计思想,可以应用在很多设计场景中,比如系统、模块、lib、组件、接口、类等等的设计。封装和抽象可以有效地隐藏实现的复杂性,隔离实现的易变性,给依赖的模块提供稳定且易用的抽象接口。

(2)中间层

引入中间层能简化模块或类之间的依赖关系。下面这张图是引入中间层前后的依赖关系对比图。在引入数据存储中间层之前,A、B、C 三个模块都要依赖内存一级缓存、Redis 二级缓存、DB 持久化存储三个模块。在引入中间层之后,三个模块只需要依赖数据存储一个模块即可。从图上可以看出,中间层的引入明显地简化了依赖关系,让代码结构更加清晰。

img

(3)模块化

模块化是构建复杂系统常用的手段。不仅在软件行业,在建筑、机械制造等行业,这个手段也非常有用。对于一个大型复杂系统来说,没有人能掌控所有的细节。之所以我们能搭建出如此复杂的系统,并且能维护得了,最主要的原因就是将系统划分成各个独立的模块,让不同的人负责不同的模块,这样即便在不了解全部细节的情况下,管理者也能协调各个模块,让整个系统有效运转。

(4)其他设计思想和原则

  • 单一职责原则
  • 基于接口而非实现编程
  • 依赖注入
  • 多用组合少用继承
  • 迪米特法则

具体的内容可以跳转到前面设计思想的相关章节查看。

updatedupdated2023-06-032023-06-03
加载评论