主页

索引

模块索引

搜索页面

重构手段-单元测试

备注

最可落地执行、最有效的保证重构不出错的手段。当重构完成之后,如果新的代码仍然能通过单元测试,那就说明代码原有逻辑的正确性未被破坏,原有的外部可见行为未变

定义

单元测试由研发工程师自己来编写,用来测试自己写的代码的正确性。

单元测试相对于集成测试(Integration Testing)来说,测试的粒度更小一些。
集成测试的测试对象是整个系统或者某个功能模块,
  比如测试用户注册、登录功能是否正常,是一种端到端(end to end)的测试。
而单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试。

备注

涉及到数据库的确实比较难写单元测试,而且如果重度依赖数据库,业务逻辑又不复杂,单元测试确实没有太大意义。这个时候,集成测试可能更有意义些。

作用

备注

单元测试除了能有效地为重构保驾护航之外,也是保证代码质量最有效的两个手段之一(另一个是 Code Review)。

  1. 单元测试能有效地帮你发现代码中的 bug:

    节省了我很多 fix 低级 bug 的时间,能够有时间去做其他更有意义的事情
    坚持写单元测试是保证我的代码质量的一个“杀手锏”,也是帮助我拉开与其他人差距的一个“小秘密”。
    
  2. 写单元测试能帮你发现代码设计上的问题:

    代码的可测试性是评判代码质量的一个重要标准。
    对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很吃力,
      那就意味着代码设计得不够合理,比如,没有使用依赖注入、大量使用静态函数、全局变量、代码高度耦合等
    
  3. 单元测试是对集成测试的有力补充:

    程序运行的 bug 往往出现在一些边界条件、异常情况下,
    而这种异常大部分情况都比较难在测试环境中模拟。
    而单元测试可以利用下一节课中讲到的 mock 的方式,
      控制 mock 的对象返回我们需要模拟的异常,来测试代码在这些异常情况的表现。
    
    对于一些复杂系统来说,集成测试也无法覆盖得很全面。
    复杂系统往往有很多模块。
    每个模块都有各种输入、输出、异常情况,组合起来,整个系统就有无数测试场景需要模拟,
      无数的测试用例需要设计,再强大的测试团队也无法穷举完备。
    
    尽管单元测试无法完全替代集成测试,
    但如果我们能保证每个类、每个函数都能按照我们的预期来执行,
    底层 bug 少了,那组装起来的整个系统,出问题的概率也就相应减少了。
    
  4. 写单元测试的过程本身就是代码重构的过程:

    写单元测试实际上就是落地执行持续重构的一个有效途径。
    设计和实现代码的时候,我们很难把所有的问题都想清楚。
    而编写单元测试就相当于对代码的一次自我 Code Review,
    在这个过程中,我们可以发现一些设计上的问题(比如代码设计的不可测试)
    以及代码编写方面的问题(比如一些边界条件处理不当)等,然后针对性的进行重构。
    
  5. 阅读单元测试能帮助你快速熟悉代码:

    阅读代码最有效的手段,就是先了解它的业务背景和设计思路,然后再去看代码,这样代码读起来就会轻松很多。
    在没有文档和注释的情况下,单元测试就起了替代性作用。
    单元测试用例实际上就是用户用例,反映了代码的功能和如何使用。
    借助单元测试,不需要深入的阅读代码,便能知道代码实现了什么功能
    
  6. 单元测试是 TDD 可落地执行的改进方案:

    单元测试正好是对 TDD 的一种改进方案,先写代码,紧接着写单元测试,
    最后根据单元测试反馈出来问题,再回过头去重构代码。
    

如何编写单元测试

  1. 编写单元测试尽管繁琐,但并不是太耗时:

    单元测试的代码量可能是被测代码本身的 1~2 倍,写的过程很繁琐,但并不是很耗时。
    毕竟我们不需要考虑太多代码设计上的问题,测试代码实现起来也比较简单。
    
  2. 我们可以稍微放低对单元测试代码质量的要求:

    单元测试不在线上运行,所以相比正式代码可以稍稍降低要求
    
  3. 覆盖率作为衡量单元测试质量的唯一标准是不合理的:

    单元测试覆盖率是比较容易量化的指标,常常作为单元测试写得好坏的评判标准
    不管覆盖率的计算方式如何高级,将覆盖率作为衡量单元测试质量的唯一标准是不合理的。
    实际上,更重要的是要看测试用例是否覆盖了所有可能的情况,特别是一些 corner case
    
    过度关注单元测试的覆盖率会导致开发人员为了提高覆盖率,写很多没有必要的测试代码
    
  4. 单元测试不要依赖被测代码的具体实现逻辑:

    它只关心被测函数实现了什么功能。
    否则,一旦对代码进行重构,在代码的外部行为不变的情况下,对代码的实现逻辑进行了修改,
    那原本的单元测试都会运行失败,也就起不到为重构保驾护航的作用了,也违背了我们写单元测试的初衷。
    
  5. 单元测试框架无法测试,多半是因为代码的可测试性不好:

    写单元测试本身不需要太复杂的技术,大部分单元测试框架都能满足。
    在公司内部,起码团队内部需要统一单元测试框架。
    
    如果自己写的代码用已经选定的单元测试框架无法测试,那多半是代码写得不够好,代码的可测试性不够好。
    这个时候,我们要重构自己的代码,让其更容易测试,而不是去找另一个更加高级的单元测试框架。
    

为何难落地执行

写单元测试确实是一件考验耐心的活儿:

一般情况下,单元测试的代码量要大于被测试代码量,甚至是要多出好几倍。
很多人往往会觉得写单元测试比较繁琐,并且没有太多挑战,而不愿意去做。

有很多团队和项目在刚开始推行单元测试的时候,还比较认真,执行得比较好。
但当开发任务紧了之后,就开始放低对单元测试的要求,一旦出现『破窗效应』,慢慢的,大家就都不写了

国内研发比较偏向“快、糙、猛”:

程序员这一行业本该是智力密集型的,但现在很多公司把它搞成劳动密集型的,
写好代码直接提交,然后丢给黑盒测试狠命去测,
测出问题就反馈给开发团队再修改,测不出的问题就留在线上出了问题再修复

关键问题还是团队没有建立对单元测试正确的认识:

觉得可有可无,单靠督促很难执行得很好。

影响可测试性的 Anti-Patterns

  1. 未决行为:

    所谓的未决行为逻辑就是,代码的输出是随机或者说不确定的
    
  2. 全局变量:

    全局变量是一种面向过程的编程风格,有种种弊端。滥用全局变量也让编写单元测试变得困难
    
  3. 静态方法:

    静态方法跟全局变量一样,也是一种面向过程的编程思维。
    在代码中调用静态方法,有时候会导致代码不易测试。
    主要原因是静态方法也很难 mock
    
  4. 复杂继承:

    相比组合关系,继承关系的代码结构更加耦合、不灵活,更加不易扩展、不易维护。
    实际上,继承关系也更加难测试。这也印证了代码的可测试性跟代码质量的相关性。
    
    利用组合而非继承来组织类之间的关系,类之间的结构层次比较扁平,
    在编写单元测试的时候,只需要 mock 类所组合依赖的对象即可。
    
  5. 高耦合代码:

    如果一个类职责很重,需要依赖十几个外部对象才能完成工作,代码高度耦合,
    那我们在编写单元测试的时候,可能需要 mock 这十几个依赖的对象。
    不管是从代码设计的角度来说,还是从编写单元测试的角度来说,这都是不合理的。
    

主页

索引

模块索引

搜索页面