自动化测试

目标

为了更好的实施自动化测试,我们希望有一套简单但足够的自动化测试体系,能够从各个层面帮助我们开展自动化测试,从而交付高质量的软件代码。

本文包含三个部分,包括「测试流程」、「测试平台和工具」和「测试方法论」,各自关注不同的维度,整体形成一个良好的体系。

测试流程

测试流程关注测试开发的过程中,应该以怎样的流程来实施,以便测试的最佳效果。

用例设计

测试用例的设计需要考虑多个层面,尽量全方位的考虑所有场景,包括但不限于如下层面:

  1. 用户故事,特定功能需要满足的用户场景
  2. 权限需求,不同权限可能的差异
  3. 异常场景,一些异常可能出现的问题,如何恢复
  4. 兼容性,对环境、设备、浏览器的要求
  5. 性能层面,数据量是否很大,是否有极致的性能需求
    这上面的东西,是测试用例设计的需求,在需求明确之后,就可以针对它们设计具体的测试用例。

好的测试用例是一个完备的集合,覆盖所有等价类与关键值,设计好的测试用例有三种常用的方式:

  1. 等价类划
  2. 边界值
  3. 错误推测

具体步骤

测试用例的设计可以分为以下几个步骤:

  1. 画一个较粗的业务流程图,包含所有的流程分支,图内每个节点可以较粗,比如“短信验证码登录”就是一个节点,先不用细分手机号是否已注册,或者对应的用户是什么身份。
  2. 我们把流程图中最长的主流程称为基本流 1,其他分支也依次标号,然后就可以组合出该流程图中所有可能的子流程,比如 1 + 2 + 4。
    ○ 当流程图比较复杂导致组合情况过多时,可以寻找解耦点將它拆分为两个甚至多个彼此之间耦合度较低的流程图,也就是说一个流程以哪一条路径执行基本不影响另一个流程的正确性。然后对拆分后的流程图罗列组合。
    ○ 我们可以进一步降低流程图的复杂度,不把“表单验证失败”的节点写在流程图中,每个表单的验证可以做成单独的测试用例。比如“短信验证码登录”的失败原因分为手机号格式不对、验证码错误、验证码过期等等,这些可以单独汇总在一个测试用例中去测试。
  3. ~~对上述的组合结果进行精简,依然从耦合度的角度去考虑。假设我们的组合中有 1 + 2、1 + 3 和 1 + 2 + 3,而分支 3 的执行与否对分支 2 的正确性几无影响,那么在已测试 1 + 2 和 1 + 3 的基础上,1 + 2 + 3 的性价比非常低,对测试效果的提升很小,可以被精简掉。这个比较依赖设计者的经验去判断哪些是“无意义”的组合。~~
    ○ 这一条先注释掉,不太好解释和实施,需要在实践中再打磨。
  4. 为精简后的每一个组合编写一个测试用例。对于用例中的每一个节点步骤,此时要考虑它的细分场景。比如“短信验证码登录”,需要先按业务场景拆分为学生登录、老师登录、家长登录、未注册手机号登录 4 种可能性。
    ○ 原则上每一种可能性都应当是一个单独的测试用例,但为了不重复编写差别很小的用例,我们可以在一个用例中描述一个节点的多个输入值。在实际执行用例时,可以选择在一个节点上测试多个输入值再继续往下也可以每个输入值都跑一遍用例中的流程,这个也需要执行者依靠经验来灵活判断。
    ○ 我们需要同时在测试用例管理工具中保存业务流程图,为执行者理解测试用例和设计者将来随着业务变化更新测试用例提供方便。
    ○ 设计测试用例是一个再次检查产品逻辑严密性的好时机,遇到问题需要及时与产品经理沟通。
    ○ 很多用例,尤其是表单验证类的,需要用等价类划分和边界值的方法去设计多种输入值。
  5. 然后设计者需要基于对业务的了解补充针对不在正常业务逻辑中的异常场景、安全性敏感场景、性能敏感场景的测试用例,就得到了完整的冒烟测试和黑盒测试集。
    ○ 有些“副作用”式的功能可以考虑集中在一个测试用例中描述,比如任务的推送,可以写一个测试用例集中测试,包括任务的创建、修改、驳回等各种情况的推送。
  6. 最后我们需要根据黑盒测试集做 E2E UI 自动化测试,考虑到编写效率和运行效率,思路是一长多短:
    a. 在一个模块的用例中取一个最长的基本流先实现。
    b. 实现其他未被覆盖的短分支。

举个例子

这是一个 App 用手机号登录后选择身份进入主页的流程图,按照上文我们提供的方法:
test

  1. 这个流程原本属于一个更长的流程,前面是手机号登录的流程,两部分加起来是一个完整的 App 登录流程,但是我们认为这个子流程之间的耦合度不高,所以选择了在中间解耦,变成两个流程分别测试。
  2. 在编写黑盒测试用例时,我们得到的组合集是:
    a. 1 -> 2 -> 3 -> 4 -> 5
    b. 1 -> 4 -> 5
    c. 1 -> 4 -> 3 -> 4 -> 5
    外加一个绑定新身份时的表单验证。
  3. 在编写 E2E UI 自动化测试时,我们先实现最长基本流,然后实现剩下的短分支,所以最后编写的用例是:
    a. 1 -> 2 -> 3 -> 4 -> 5
    b. 1 -> 4 -> 5
    c. 1 -> 4 -> 3

测试规划

在需求明确之后,有大致的测试用例之后,应该对测试的实施有一个大致的规划,一定程度的明确各个类型的测试做多少,怎么做。

比如对于复杂的项目,我们可能需要更多的单元测试,从而保证逻辑的正确性,对于简单的项目,可能一定的功能测试和 E2E 测试就可以了。

测试规划的时候,应该考虑到各种测试类型擅长的和不擅长的,合理安排比例以及覆盖场景,在成本可控的前提下实现效率的最大化。

测试开发

规划完毕之后,就可以和代码开发一起,进入测试开发的环节,在开发的过程中,一定会发现之前没有考虑到的情况,或者发现效率更高的方法,这时及时调整策略,优化方案。

测试平台与工具

测试平台和工具包含测试工具相关的东西,通过优秀工具的使用和研发,让测试开发能够非常简单的进行,找到测试开发的乐趣。

测试数据管理平台

测试数据是进行高效测试的基础,尤其是对于我们这种 toB 复杂业务逻辑来说,如果没有良好基础测试数据的支持,我们将花费大量时间在测试数据的准备上,测试的编写和维护将会变成一件很难受的工作。

为了解决这个问题,我们引入测试数据管理平台,他能够帮助我们管理各种测试数据,需要的时候,通过很简单的方式就可能调用这些测试数据,减少人工构造的成本。

这之中的测试数据将主要用到以下四个地方:

  1. 后端功能测试
  2. 前端 E2E 测试
  3. demo 系统
  4. 开发环境

对这个平台而言,它将提供如下功能:

  1. 测试数据存储与管理
  2. 测试数据的写入与生成
  3. 多套后端服务的配置管理,同一个平台,可以向多个后端写入数据?
  4. 通过对外 API,能够通过 API 执行指定的命令
  5. 依据 client 请求执行单个/批量数据请求/写入操作

目前这个平台主要提供一套相对固定的数据, 将来,它应该有能力按需及时生成一部分数据,能够用于对数据之间关系要求不高的场景。

后端测试工具

为了改善后端测试的效率,我们已经/需要开发一些测试工具,让测试开发更简单,主要包含如下几个部分:

  1. 基础测试工具,是对开源工具的改善和封装,比如 UTL https://git.seiue.com/open-source/utl/tree/master
  2. 应用层相关工具,比如 https://www.yuque.com/kovru3/gfdy75/ygtibk 这里面说的
  3. 与测试数据平台的交互,交互方式是什么?类似 UDS 注册的机制?
  4. Reponse 与 schema 定义的一致性检查

前端测试工具

同理,前端差不多,可能包含如下几个部分:

  1. 基础测试工具最佳实践,封装
  2. 数据 Mock 的机制,什么时候 Mock
  3. 测试数据平台通信的方式

测试方法论

编写可测试代码

为了良好的实施自动化测试,我们对各方实施一定的约束,以便能够高效的开展自动化测试,让自动化测试的效能最大化。

被测主体约束

被测试主体(包括但不限于后端服务,前端组件,数据层)都应该提供良好的数据测试支持,这样测试代码才能够很好的与测试主体进行交互,从而高效的完成测试。

比如,后端在进行 API 设计的时候需要考虑到测试的场景,能够通过 API 准备需要的测试数据,也可能需要给测试提供专用的 API。

前端可能将数据数据操作接口暴露到外部,能够让测试代码直接操作内部方法和数据,以便方便高效的完成自动化测试(见附[1])。

被测试主体应该同时满足多方面的需求,包括但不限于:

  1. 业务需求
  2. 测试需求
  3. 架构需求

代码约束

为了进行良好的单元测试,对我们的代码也提出了更高的要求,我们的代码需要更好的可测试性,具体来说包含如下几个方面:

  1. 功能划分清晰,职责分明,函数、类的实现应该高内聚,底耦合,一个方法只做一件事
  2. 外部依赖显式化,依赖的东西应该单独测试 附[2]
  3. 尽量避免副作用,输入输出应该简单统一
  4. More needed

数据与行为分离

在测试中,有很多时候我们需要对同一个场景使用不同的数据进行测试,同时期望获取不同的结果,这时如果测试数据和测试代码耦合在一起,就需要将他们同时复制多份,代码可读性和维护起来都是一个问题。

这时,我们可以采用数据与行为分离的策略,将测试行为抽象出来,然后再用给定的数据跑测试。

在 PHP 项目中,我们可以 PHPUnit 的 DataProvider 实现这样的策略,在前端的 Jest 中,也可以使用 jest.each 达成同样的效果。不同测试框架都应该有类似的支持(hmmm, Cypress 好像没有原生的支持)。

App Actions

在 E2E 测试中,传统的做法是封装 PageObject,用来封装 UI 层的操作,以便让测试代码更加易于维护,但因为 UI 层的变化一般比较大,这一层的封装成本比较高,而且所有地方都通过 UI 来操作效率很低,稳定性也不好。

所以,Cypress 采用了另外的思路,就是直接在测试代码中操作应用的内部逻辑,我们把这种方式称作 App Actions。某种程度上去掉了和简化了 PageObject 这个封装,从而减少的响应的成本。

它的缺点是需要理解应用的内部状态,但因为我们是开发者自己写测试,所以这个反而变成了优点。

读者具体可以查看 Stop using Page Objects and Start using App Actions 了解更详细的内容。

实施计划

测试数据平台
[ ] 测试平台准备工作(P0)
[ ] 方案优化空间
[ ] 环境初始化
[ ] 对外提供 API (P1)

测试数据
[ ] 用户数据 (P0)
[ ] What’s else?

测试工具
[ ] 后端测试工具整理和规范 UTL
[ ] 后端针对 Event 和 Job 的测试工具
[ ] 后端 response 与 schema 的一致性检查 (P0)
[ ] 后端与测试平台的交互机制?(P0)
[ ] 前端测试工具的实践
[ ] 前端与测试平台的交互机制 (P0)
[ ] 前端 Mock 的场景与机制

测试流程/方法论
[ ] 形成最佳实践(跟随实践不断迭代)

附录

[1] https://www.cypress.io/blog/2019/01/03/stop-using-page-objects-and-start-using-app-actions/
[2] http://www.voidcn.com/article/p-uyjitnuy-bno.html