虽然如今前端测试这个事已经被大家所认可了,但我见过做前端测试的团队并不多,能把前端测试做好的团队,就更加凤毛麟角了。
这个现象背后的逻辑是:编写前端测试其实非常困难。
在编写测试代码时,我们有很多事情要考虑,要做出的选择。有些时候甚至编写测试代码会比实现功能代码更加令人头痛。
在本文中,我将全面介绍前端测试的策略,并且探讨编写出更好的测试的原则,最终让你把前端测试这件事儿做好。
为什么需要前端测试?
测试的主要作用是能够给你带来信心。
什么意思?
你增加了一个功能,但是你不确定这个功能是否会破坏之前的功能。如果你不写测试,这个功能就像薛定谔的猫,无论你认为这个功能是稳定的还是存在风险的,都是你的臆想,没有任何方法可以证明它到底如何。
如果你有一个严谨的测试团队还好,他会帮你把问题提前找出来。
但是如果你没有一个很好的测试帮你找 Bug,怎么办呢?
你只能颤颤巍巍的把代码提交到 git 上,点击 publish。然后彻夜难眠,等待用户帮你验证功能,听天由命。
如果你写了测试,那么它可以自动帮你验证新功能是否会影响老功能。所有的测试用例都 pass 了,你就再也不怕了,终于可以安心回家睡觉了。
也就是说,测试能够提高代码质量,让你可以相信自己写的东西。
我们可以做哪些类型的测试?
我们如果只是笼统的聊测试,根本无从下手。因为测试一个软件的方式有非常多。
所以我们先把测试进行归类,然后逐个介绍它们,以及如何实施它们。
在传统的测试中,已经有大量的类型了,它们包括:A/B 测试、性能测试、冒烟测试、回归测试等。每一种测试针对的点都不同。
除了这些类型,从实施测试的方式上也可以分为人肉测试和自动化测试。
自动化测试就是通过代码的方式自动执行测试。
其实上面聊的测试方式,都和前端关系不大。那些都是专业测试该干的事。
前端的测试,主要是自动化测试。
聊到自动化测试,基本上可以分为四种:静态测试、单元测试、集成测试、端到端测试。
接下来我会详细聊聊它们。
静态测试
静态测试就是指不需要额外运行代码就能执行的测试。
一般来说我们都在无时无刻的用着它们,比如类型检查工具 TypeScript 和代码样式检查工具 ESLint。
单元测试
单元测试是把软件分成了多个小而独立的部分,比如模块或函数,通过对这些小单元进行测试,就是单元测试。
常用的工具是 Jest。
集成测试
集成测试比单元测试更加复杂,它的目的是把多个单元组合起来进行协作。
每个单元都正常,但是大家组合起来未必正常。所以这就是集成测试的必要性。
Jest 也可以做集成测试。
关于集成测试,可以举个让我印象深刻的例子。
有一个品牌的汽车门老是响,但是去质检,发现车子是合规的。原因是生产车体的螺丝槽有上下工差,是 0.5cm-1cm。生产车门的螺丝槽也有上下工差,是 0.5cm-1cm。两者都在这个区间,就视为合格。但是生产车体的人按最差的去做,造成了 1cm 误差,生产车门的人也按最差的去做,也造成了 1cm 误差。但是对两者单独检测,都是合格的。但是组合到一起用,就形成了 2cm 的误差,导致车门发出异响。
端到端测试
端到端测试的覆盖面比集成测试更广泛。它会以类似用户实际操作的方式对整个应用进行测试。
在前端中,它会模拟浏览器,模拟用户登录、页面交互等。
常用的工具是 Cypress。
为什么人肉测试会比自动化测试更流行?
原因有很多,我认为主要有几点。
第一是写自动化测试代码老板不给钱。
第二是人肉测试更加简单直接,而且接近最终用户的体验。但是做人肉测试非常麻烦,因为每改动一个功能,都需要人从头到尾进行测试。但是人肉测试的人工资远比程序员低,也就是说虽然人肉测试很麻烦,但和老板关系不大,性价比极高。如果人肉测试工程师的价格和程序员的价格一样的话,那自动化测试就会更加流行。可以从一个现象来说明这一点:自动化测试工程师薪资会比人肉测试工程师更贵。
最后再解释一下为什么有人认为人肉测试会比自动化测试更专业?
因为自动化测试往往是开发应用的人做的,程序员大多比较理性,逻辑通常也比较严谨,他自然知道自己的应用支持哪些功能。但是这也是一个缺点,程序员很容易陷入到自己的逻辑里面跳不出来。而人肉测试会包含很多感性的部分,这是超越理性的东西,可以看到程序员自己看不到的内容。这也是人肉测试在测试领域占有一席之地的第三个原因。
我们应该选择哪种测试?
\
编写测试其实就是在权衡,在考虑成本。我们没办法在测试这件事上投入过多的精力。通常只能拿出小部分的时间去写测试代码,当然 TDD 的开发模式除外。
在测试领域,有一个测试金字塔的概念。
它最早出现在 马丁福勒的博客 中以及 Google 测试博客。
越朝上的测试,运行和维护的成本越高,但是越有效。
越朝下的测试,运行和维护的成本越低,但是效果越差。
这个模型是 2012 年提出来的,现在看来有些过时了。
现在更流行的是测试奖杯。
测试奖杯在最下面加入了 Static,也就是静态测试。
测试奖杯模型的主要观点是,端到端测试很好,但是成本太高,不应该过度测试;集成测试是最广泛的一层,应该集中精力做集成测试;单元测试应该是最少的一部分;静态测试就像是奖杯的基座,应该存在与程序的任何地方。
最后总结一下。以我的经验来看,如果要开发应用,集成测试优先,其次是端到端测试,最后是单元测试。
如果是开发库,那么主要是单元测试。
应该追求测试覆盖率吗?
很多人喜欢追求 100% 的测试覆盖率。也就是说要让所有的代码都要覆盖到。
这是要分两种情况的。
先来聊第一种情况,就是测试应用。
在测试应用时,我们做到大概 60-70% 的代码覆盖率后,再想继续提升覆盖率就很困难了。
我们应该去测试用户会用到的内容,而不是所有的代码细节。像是一些私有函数或者内部实现的细节没必要测试,因为用户根本不会使用这些东西。
我们可以根据用户能够操作的内容去编写用例,然后去保证测试用例的 100% 覆盖率。因为用例的变化没有那么频繁,但是代码的变化非常频繁。
第二种情况,如果你是在做一个基础库,那么追求 100% 覆盖率无可厚非。
前端测试的原则
接下来我再聊聊我们应该遵循哪些原则,来让我们把前端测试这件事做好。
注意这些原则主要是针对应用的开发者,而不是库的开发者。
把自己当作用户
很多人测试,会从程序员写代码的角度出发,这是不对的。
我们要跳出来,把自己想象成一个普通用户。只有像普通用户那样去点击按钮,在输入框中输入文字,才能真正发现用户的问题。
避免测试细节
不要花费时间去测试私有函数或者某些具体实现。
不要去测试组件内部的状态、方法、生命周期之类的东西。
不要去测试组件内部的结构。
因为这些东西都是随时可能被我们重构的,比如我把随机 id 从 uuid 改为 nanoid,把 div 元素更换为 header 元素等。但是这些变动并不会直接影响用户。
编写数量更少、但覆盖内容更多的测试
很多人在写测试的时候喜欢把写代码的思维给带进来,按照单一职责原则,把用户的一个流程测试用例拆分成了一堆测试用例。这样你需要不断地模拟上下文。
仔细想想,这样有必要吗?
没有任何理由可以说明这样做是有好处的。
不用担心一个测试用例很长,只要这个测试用例的规则符合用户的一个操作流程就可以了。
编写易于理解和可维护性的测试
和上一条原则类似。
我们在写代码时喜欢抽象、封装、继承。
但是测试代码和代码是不一样的。我们不要害怕在测试代码中出现类似重复代码之类在代码最佳实践中被视为坏味道的东西。
测试代码的主要原则是能非常容易地被人理解,所以测试代码中不应该有复杂度的存在。
单独编写测试
测试用例之间不应该相互依赖。
也就意味着每个测试用例之间的结果不会影响其他测试用例的结果。而且我们应该可以用任意的执行顺序去执行它们,测试结果始终不变。
也就是说每个测试用例之间都会有自己的独立的上下文。
而且如果测试之间没有依赖关系,那么就可以并行运行所有测试,节省测试的总运行时间。
不要太相信模拟的东西
很多时候我们需要模拟一些数据来方便测试。如果操作真实业务可能会有些麻烦。
但是测试数据往往不能和真实数据一一对应。比如我们在模拟数据中模拟了 11 位的手机号,但是生产环境中可能会有老外输入其他位数的手机号。在编写测试时我们很可能考虑不到。
也就是说即使我们在测试环境中实现了 100% 的测试,在生产环境中仍然可能会出问题。
所以从开始模拟的那一刻起,你就应该考虑到我们不可能模拟 100% 的真实场景。
总结
总结来说,前端测试难做,就是因为需要权衡和需要共情。
大多数程序员按照具有明确目标的指引去写代码完全没问题,但是一旦涉及到需要权衡的时候,就不太擅长了。
另一方面,程序员群体也比较专注于自己的领域,认为代码写得好就是好程序员。而测试的一个要点就是要从用户角度出发,换位思考、同理心、共情,都是程序员比较缺乏的东西。
所以能不能做好测试,主要取决于团队整体有没有真正理解测试的意义和价值,能否做出合适的权衡,以及是否拥有共情。和 Jest 或者 Cypress 用的熟不熟基本上没关系。这也是很多技术很强的团队依然做不好前端测试的原因。
最后希望读完本文,对你未来做好前端测试这件事能够有一些帮助。