logo

鱼肚的博客

Don't Repeat Yourself

React测试驱动开发 - 理论篇

前端业务开发中如何做到测试驱动?怎么样平衡测试的效果与维护的成本?

这些问题我摸索了挺长时间,也略微有了一些心得,在这里总结一下。

测试的意义

在开始之前,先问几个问题:

为什么要做测试?

质量!质量!质量!

测试当然是为了质量,这是毋庸置疑的。

为什么开发要做测试?

  1. 可以提升交付物质量,避免被老板、QA等骂
  2. 可以沉淀测试用例,对以后重构、技术升级有帮助。

为什么要测试驱动开发?

  1. 提前思考测试用例,可以早点发现不确定的需求
  2. 有助于避免返工

当然这一点见仁见智,有的人可能倾向于认为,先开发再测试是比较好的。

然而我的经验告诉我,凡是 “等以后有时间再做XXX” 的,基本就没戏。所以要做测试的话,最好就是直接测试先行。

任务太紧了,没有时间写自测用例

这个问题要从两个方面来看:一是是否真的没有时间写用例;二是有没有高估写用例所需的时间?

一方面,在业务开发的过程中,一般会经过需求评审、技术评审等阶段,涉及到需求的讨论、前后端接口的讨论等,在这些阶段中,可以抽出一部分时间写用例,它对于“需求评审”和“技术评审”这两个阶段也是相辅相成的。

所以,如果能打好时间差,有效地利用业务开发前的方案评审制订时间,写用例的时间可以压缩不少。

另一方面,虽然大家口头上承认单元测试的好处,但是实际开发中很少有人用到。这就导致大家对测试用例应该怎么写这件事很不熟悉,也自然地会高估其所需的时间。

而实际上,当测试流程用得比较熟悉之后,写用例所需的时间是会比一开始的时候大大降低的。

前端工程不适合写测试用例?

有些人认为前端的页面变化较快,用例的保质期很短。随便一个div的结构变化,就会导致测试失效。

这个主要也是写测试的时候的一些方法论的问题,怎么样写出来保质期较长的测试用例,也是需要学习和总结的。关于这一点,后面再展开来写。

与自动化测试的工作重合

自动化测试团队,一般认为属于测试团队的一部分。他们会使用诸如 selenium + webdriver、cypress、testcafe等做一些端到端的测试。

如果说他们已经有了相关的自动化测试用例,那么前端开发再做一份单元测试会不会浪费工作量呢?

这里我的答案是不会,因为前端的单元测试相比于自动化测试有如下的长处:

  1. 时效性好:单元测试在功能尚未交付时,就可以开始构建。而自动化测试团队一般需要在功能交付之后,再对一些重点功能做测试。俗话说远水解不了近渴,它对开发中的功能的质量保证是没有帮助的。
  2. 性能好:不同于自动化测试需要完整的浏览器运行环境(即使是无头浏览器),单元测试可以直接针对代码做白盒测试。因为过程中不涉及到真实dom渲染等与浏览器的交互,也可以mock一些慢接口,所以性能会好一些。
  3. 可用性好:通过mock等手段,可以使前端的单元测试不依赖后端接口,这样即使后端服务出了问题,也不影响前端测试的可用性。另外因为它使用前端开发套件就可以运行,不需要安装selenium、cypress、python等测试套件,所以对前端开发来说更方便易用。
  4. 用例丰富:因为性能等综合原因影响,自动化测试中不能写很多很细的用例,不然时间会大大增加。这方面单元测试要好很多。

另外,写单元测试对自动化测试也会产生一些正面的作用。如开发在单元测试过程中,为了方便书写用例,会使页面中带有一些易用的选择器属性(如自定义属性、类名等),这样自动化测试团队也可以从中受益,减轻写选择器时的工作量。

当然,这里并不是说不需要关注测试重复的问题,每一个测试用例背后都有着一系列的开发维护和理解的成本,同时也会带来运行时的成本。如果有多个团队在从不同维度做自动化测试,在设计测试用例的时候,应当提前沟通好各自覆盖的领域。发挥各自擅长的部分,协同完成整个自动化质量体系的构建。


下面介绍下我总结的测试的一些理论。

测试的原则

这里我要援引以下两篇文档,有兴趣的同学推荐看下:

  1. practical-test-pyramid
  2. testing-library guiding-principles

测试领域有一个经典的金字塔模型:

它表达了如下的观点:

  1. 测试可以从多个不同的维度进行
  2. 越接近用户真实体验,测试的独立性越差,性能越差。

按照测试金字塔的理论,我们应该写大量的单元测试、中等数量的集成测试、以及少部分的端到端测试。

但是同时testing-library的理论中也指出,测试越是靠近最终使用场景,越能带来更高的信心。

image-20210429140327350

在我看来,传统的测试理论(如测试金字塔)到现在仍然有效,但是它们更多的是偏重逻辑端,对现在的前端开发领域难以形成良好的理论指导。如前端的测试是否属于测试金字塔的最顶端?有没有办法做单元测试?这些是缺乏理论指导的。

Testing-library 中提出的测试理念,以及一系列测试套件,以接近集成测试的成本,提供了类似端到端测试的体验,是一种比较友好的测试理论和框架。

后面我要说的就是如何利用testing-library 测试前端应用,给出一些理论参考。

总结下我认为前端业务系统的开发人员编写测试代码的原则是:

  1. 应当尽可能独立。

    前端的测试代码应当尽可能地不依赖后端接口。当需要使用接口时,可以使用mock工具进行模拟。

  2. 应当有较好的保质期。

    前端的测试代码应当尽可能地从用户角度出发,不涉及到具体的技术细节。诸如snapshot测试等,因为一个微小的dom结构变化就失效的测试,不应该是主要的测试手段。

  3. 应当有较低的运行成本。

    每次运行的成本应尽可能地低,避免等待API接口等耗时操作的产生。

  4. 应当尽可能从用户角度进行测试(纯逻辑单元除外)。

    越靠近用户真实使用场景,测试越有效果。

写出便于测试的代码

测试的复杂性,不仅取决于业务的复杂度,也取决于代码的实现方式。

业务复杂度是很难改变的,而代码的实现方式比较容易更改,尤其是在提前考虑到测试问题的时候。

代码的实现方式从哪些方面影响测试复杂度?

锚点的选定

对前端测试而言,锚点的选定是至关重要的,首先确定元素的位置,然后才能进行接下来的操作。

如果在编写代码时,没有考虑到测试的需求,那么这个步骤可能会很困难。

定位元素的过程,会用到querySelector等API,一般会使用classtagNamexpath等属性进行定位。

示例:

1// 根据xPath定位元素
2function getElementByXpath(path) {
3  return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
4}
5getElementByXpath("//*[@id="rso"]/div[1]/div[1]/div/div[2]/div[1]/a/h3")
6
7// 根据 class 定位元素
8document.querySelector(".myclass");
9
10// 根据自定义属性定位元素
11document.querySelector("[data-testid='审核通过按钮']")

不同的锚点选定方式,又是怎么影响了测试的复杂度呢?

这又要从两个维度来说:

  1. 锚点可读性:自定义属性定位元素 > class >> xPath
  2. 锚点保质期:xPath ~= class >> xPath

稍微解释一下:

首先,从规范的角度来说,class是不推荐写中文的,且有一定的命名规则,不能随意书写。所以能写中文的自定义属性(可以是data-testid,或data-xxx,随便写)可读性会大于 class。class虽然可读性没那么好,但至少还能用于代码搜索,而 xPath 既不可读,也没办法用于代码搜索,属于可读性最差的实现方式。

然后,因为前端页面经常会有结构上的变化。这个时候xPath会跟随变化,继而导致测试用例失败。而class和自定义属性则可以保持不变。所以使用自定义属性和class作为锚点选择器,保质期会更长。

因此,如果开发人员能在实现功能时,主动加上便于测试的自定义属性(如data-testid),或class,会大大降低测试时锚点的选择难度,以及大幅提高其保质期。

代码的模块化

单元测试除了测功能,也可以直接引入给定的函数、类等进行测试。

如果将一些纯逻辑的单元独立成模块导出,就可以很容易地写一些针对此部分的单元测试。

而如果将代码全部糅合在一起,测试时就很难针对一小部分功能独立测试了。


以上就是本文的全部内容啦,后面我会逐渐更新一些具体的问题处理和实践指南,敬请期待。