[Diving into WWDC 2017] Engineering for Testability

测试的工程化

一、前言

单元测试可以帮助开发人员持续验证代码能否正常工作。但是,编写单元测试依赖现有代码的可测试性,开发人员需要不断探索代码重构的技术,确保代码容易被测试,通过重复的循环迭代,形成最佳实践和一套对应的测试集,伴随着应用一同迭代。

二、可测试的代码

在现有代码中引入单元测试的作用主要有 3 点:

  1. 确保代码按照预期运行
  2. 防止代码迭代出现退化现象
  3. 作为可执行的文档供参考

在现有代码中引入测试,首先,得分析代码的可测试性,可测试的代码应该具备输入可控,输出可见和没有隐含状态等特征。然后,按照准备输入-执行动作-验证输出的路径分析干扰代码可测试性的因素,最后,通过一定层次的抽象,解耦依赖问题,让数据的输入和输出都达到自我完备 (fully self-contained),从而使单元测试变得可行。

从测试重构技术来归纳,一是协议和参数化,二是隔离逻辑和执行 (effects)

协议和参数化

Session 演讲者 Brain Croom 举例了一个业务和 UI 相耦合的测试例子。说明测试这样的 Case,有两种途径,即 UI 测试和单元测试。UI 测试路径长,测试运行时间长,并且有限制条件,即无法验证结果的 UI 细节等,在这样的 Case 里 UI 并不适用。与之相比,更加可行的是单元测试,把其中的逻辑部分做单元测试,整体上做集成测试。

重构前代码

从代码分析来看,openTapped() 方法的输入并不明确,结果并不容易验证,且 open() 存在副作用。故有必要对代码进行重构,以满足 输入-动作-输出 这样的测试条件。重构的步骤是将对象依赖通过参数的形式传入,行为通过协议的方式来声明,另外,通过 Mock 对象实现协议来控制行为,从而控制结果输出。

重构后代码

隔离逻辑和效果

演讲者举例了一个算法和文件系统相耦合的例子。代码分析来看,函数的数据输入和输出都依赖文件系统,无法直接控制和测试。重构的步骤是通过抽象协议的方式,将算法逻辑和执行逻辑隔离开,具体的算法支持可替换,而把具体的执行逻辑交给集成测试来覆盖。

在执行逻辑之上抽象出来的算法逻辑采用函数式编程方式,输入和输出均采用值类型,算法函数本身没有副作用。

重构后代码
重构后代码

三、测试代码规模化

测试代码规模化是为了让测试代码运行更快,更具可读性和更模块化。

在 UI 测试和单元测试之间寻求平衡

测试金字塔 (Testing Pyramid) 能够很好地描述大部分测试的分布结构,因为运行速度上的差异,单元测试总是比 UI 测试更多,处于两者中间是集成测试。另一方面,单元测试比 UI 测试具有更低的维护成本,一旦出错就能看出是什么地方出错。

测试金字塔

对比来看,单元测试在测试细小的,难以触达的代码路径方面作用突出,而 UI 测试则在更大规模的代码片段的集成测试方面更具优势。

UI 测试的辅助代码

  1. 抽象 UI 元素获取

    • 存储重复使用的 UI 元素
    • 封装复杂的元素获取方法
    • 减少 UI 测试中的干扰因素
  2. 抽象对象和工具函数

    • 封装常用的测试流程
    • 跨平台代码共享
    • 提升可维护性,例如使用 XCTContext.runActivity() {}
  3. 有效利用快捷键(针对 macOS 平台的 UI 测试)

    • 避免直接操作 menu bar
    • 让测试代码更简洁

测试代码质量

  1. 测试代码很重要,即便他们未被包含在产品之中
  2. 测试代码应该支持应用的演进
  3. 代码规范同样适用于测试代码

四、总结

单元测试和 UI 测试能够在不同层次上对应用的质量提供保障。在现有代码中引入单元测试的重构代价是阻挠开发人员编写单元测试的一个重要因素,此 Session 通过示例阐述了编写可测试代码的相关技术和理念,包括依赖对象参数化,引入协议,Mock 对象等重构技术,也包括将逻辑和执行相隔离的设计理念。另外,对于 UI 测试,提供了很实用的技巧,包括抽象 UI 元素获取,封装常用测试流程,利用键盘快捷键等。同时,介绍了一个新的 API XCTContext.runActivity,它有利于提升 UI 测试代码的可维护性。最后,强调了测试代码和应用代码同样重要,理应同等对待,保证让测试代码同应用代码一同迭代。

如果读者还想进一步了解和实践单元测试,推荐阅读《测试驱动的面向对象软件开发》作为参考,此书详尽介绍了编写单元测试过程中需要的重构技术,相信会对单元测试实践有所帮助。

五、参考文献

  1. https://github.com/mvemjsun/Xcode83
  2. https://developer.apple.com/documentation/xctest/activitiesandattachments/groupingtestsintosubstepswith_activities