概述

在开发过程中总会经历下面类似的问题:

  • 每次在版本发布上线之前,要花费好几个小时甚至是更长时间对你的应用进行测试,这个过程非常枯燥而痛苦。
  • 当代码的复杂度达到了一定的级别,当维护者的数量不止你一个,你应该会逐渐察觉到你在开发新功能或修复 bug 的时候,会变得越发小心翼翼,即使代码看起来没什么问题,但你心里还是会犯嘀咕:这个 Feature 会不会带来其他 Bug ?这个 Fix 会不会引入其他 “Feature” ?
  • 当你想要对项目中的代码进行重构的时候,你会花费大量的时间进行回归测试(对以往的功能进行测试)

以上这些问题都是由于大多数开发者所使用最基本的手动测试的方式所带来的问题,解决它的根本原因就在于引入自动化测试方案。

什么是应用程序测试

一个简单的定义是:应用程序测试是指检查程序运行过程是否正确。

在日常的开发中,代码的完工其实并不等于开发的完工。如果没有测试,不能保证代码能够正常运行。

如何进行应用程序测试

  • 手动测试:通过测试人员与应用程序的交互来检查其是否正常工作。
  • 自动化测试:编写应用程序来替代人工检验。

手动测试

每一个称职的开发人员都懂得手动测试代码。在编写完源代码之后,下一步理所当然就是去手动测试它。

手动测试的优势在于足够简单灵活,但是缺点也很明显:

  • 手动不适合大型项目
  • 忘记测试某项功能
  • 大部分时间都在做回归测试

虽然有一部分手动测试时间是花在测试新特性上,但是大部分时间还是用来检查之前的特性是否仍正常工作,这种测试被称为回归测试

回归测试对人类来说是非常困难的任务——它们是重复性的,要求投入很多注意力,而且没有创造性的输入。

总之,这种测试太枯燥了。幸运的是,计算机特别擅长此类工作,这也是自动化测试可以大展身手的地方!

自动化测试

自动化测试(automated testing)是利用计算机程序检查软件是否运行正常的测试方法。换句话说,就是用其他额外的代码检查被测软件的代码。当测试代码编写完之后,就可以不费吹灰之力地进行无数次重复测试。

可使用许多种不同的方法来编写自动化测试脚本:

  • 可以编写通过浏览器自动执行的程序

    • 例如通过程序打开浏览器,输入用户名密码,点击登录
  • 可以直接调用源代码里的函数
    • 使用测试方法调用函数,记录过程和覆盖率等
  • 也可以直接对比程序渲染之后的截图
    • 例如开发的组件库,可以比对快照

虽然每一种方法的优势各不相同,但它们有一大共同点:相比手动测试而言节省了大量时间以及提高了程序的稳定性

当然不仅如此,自动化测试还有很多优点,比如:

  • 尽早的发现程序的 bug 和不足
  • 增强程序员对程序健壮性、稳定性的信心
  • 改进设计
  • 快速反馈,减少调试时间
  • 促进重构

当然了,自动化测试不可能保证一个程序是完全正确的,而且事实上,在实际开发过程中,编写自动化测试代码通常是开发人员不太喜欢的一个环节。

大多数情况下,前端开发人员在开发完一项功能后,只是打开浏览器手动点击,查看效果是否正确,之后就很少对该块代码进行管理。造成这种情况的原因主要有两个:

  • 一个是业务繁忙,没有时间进行测试的编写
  • 另一个是不知道如何编写测试

主要困惑来自于:

  • 如何进行前端应用测试?
  • 应用程序中哪些部分应该被优先测试?
  • 这些部分应该使用什么方法进行测试?
  • 一些特殊场景下的测试问题该怎么解决?
  • 我们如何从一开始就整合不同的测试技巧,编制一个高效的测试套件?

这些问题不应该作为开发者掌握前端自动化测试的绊脚石,一旦掌握了前端自动化测试方案无论是应对大型项目的开发还是升职加薪都是有益的。

测试分类

前端开发最常见的测试主要是以下几种:

  • 单元测试:验证独立的单元是否正常工作
  • 集成测试:验证多个单元协同工作
  • 端到端测试:从用户角度以机器的方式在真实浏览器环境验证应用交互
  • 快照测试:验证程序的 UI 变化

单元测试

单元测试是对应用程序最小的部分(单元)运行测试的过程。通常,测试的单元是函数,但在前端应用中,组件也是被测单元。

单元测试可以单独调用源代码中的函数并断言其行为是否正确。

// sum.js
// 待测函数
export default function sum(a,b) {return a + b
}// sum.spec.js
// 将 sum 函数导入测试文件
import sum from './sum.js'function testSum() {// 如果 sum 函数不返回 2,则抛出错误if (sum(1,1) !== 2) {throw new Error('sum(1,1) did not return 2')}
}testSum()

与端到端测试不同,单元测试运行速度很快,只需要几秒钟的运行时间,因此你可以在每次代码变更后都运行单元测试,从而快速得到变更是否破坏现有功能的反馈。

单元测试应该避免依赖性问题,比如不存取数据库、不访问网络等等,而是使用工具虚拟出运行环境。这种虚拟使得测试成本最小化,不用花大力气搭建各种测试环境。

单元测试的优点:

  • 提升代码质量,减少 Bug
  • 快速反馈,减少调试时间
  • 让代码维护更容易
  • 有助于代码的模块化设计
  • 代码覆盖率高

单元测试的缺点:

  • 由于单元测试是独立的,所以无法保证多个单元运行到一起是否正确

常见的 JavaScript 单元测试框架:

  • Jest
  • Mocha
  • Jasmine
  • Karma
  • ava
  • Tape

Mocha 跟 Jest 是目前最火的两个单元测试框架,基本上目前前端单元测试就在这两个库之间选了。总的来说就是 Jest 功能齐全,配置方便,Mocha 灵活自由,自由配置。

两者功能覆盖范围粗略可以表示为:Jest === Mocha + Chai + Sinon + mockserver + istanbul

实际使用来看,目前最火的是 Jest,推荐使用。

集成测试

人们定义集成测试的方式并不相同,尤其是对于前端。

有些人认为在浏览器环境上运行的测试是集成测试;有些人认为对具有模块依赖性的单元进行的任何测试都是集成测试;也有些人认为任何完全渲染的组件测试都是集成测试。

个人笼统的认为,将多个单元组合在一起进行的测试可以称为集成测试。

集成测试从用户角度出发,不关注细节和过程,只关注结果。

优点:

  • 由于是从用户使用角度出发,更容易获得软件使用过程中的正确性
  • 集成测试相对于写了软件的说明文档
  • 由于不关注底层代码实现细节,所以更有利于快速重构
  • 相比单元测试,集成测试的开发速度要更快一些

缺点:

  • 测试失败的时候无法快速定位问题
  • 代码覆盖率较低
  • 速度比单元测试要慢

端到端测试(E2E)

E2E(end to end)端到端测试是最直观可以理解的测试类型。在前端应用程序中,端到端测试可以从用户的视角(可见的)通过浏览器自动检查应用程序是否正常工作。

想象一下,你正在编写一个计算器应用程序,并且你想测试两个数求和的运算方法是否正确。你可以编写一个端到端测试,打开浏览器,加载计算器应用程序,单击“1”按钮,单击加号“+”按钮,再次单击“1”按钮,单击等号“=”,最后检查屏幕是否显示正确结果“2”。

function testCalculator(browser) {browser.url('http://localhost:8080').click('#button-1').click('#button-plus').click('#button-1').click('#button-equal').assert.containsText('#result','2').end()
}

编写完一个端到端测试后,可以根据自己的需求随时运行它。想象一下,相比执行数百次同样的手动测试,这样一套测试代码可以节省多少时间!

优点:

  • 真实的测试环境,更容易获得程序的信心

缺点:

  • 首先,端到端测试运行不够快。启动浏览器需要占用几秒钟,网站响应速度又慢。通常一套端到端测试需要 30 分钟的运行时间。如果应用程序完全依赖于端到端测试,那么测试套件将需要数小时的运行时间。
  • 端到端测试的另一个问题是调试起来比较困难。要调试端到端测试,需要打开浏览器并逐步完成用户操作以重现 bug。本地运行这个调试过程就已经够糟糕了,如果测试是在持续集成服务器上失败而不是本地计算机上失败,那么整个调试过程会变得更加糟糕。

一些流行的端到端测试框架:

  • Cypress - 目前用的最多
  • Nightwatch
  • WebdriverIO
  • playwright

快照测试

快照测试类似于“找不同”游戏。快照测试会给运行中的应用程序拍一张图片,并将其与以前保存的图片进行比较。如果图像不同,则测试失败。这种测试方法对确保应用程序代码变更后是否仍然可以正确渲染很有帮助。

传统快照测试是在浏览器中启动应用程序并获取渲染页面的屏幕截图。它们将新拍摄的屏幕截图与已保存的屏幕截图进行比较,如果存在差异则显示错误。这种快照测试在操作系统或浏览器存在版本间差异时,即使快照并没有改变,也会遇到测试失败问题。

开发者可以使用 Jest 测试框架编写快照测试。取代传统对比屏幕截图的方式,Jest 快照测试可以对 JavaScript 中任何可序列化值进行对比。你可以使用它们来比较前端组件的 DOM 输出。

Jest 首次会将组件渲染的 html 内容作为快照保存下来,之后测时的时候会用新的 html 比对快照的 html。

快照测试可以用来测试提供给外部使用的组件库(例如 vant 对每个组件都做了快照测试),保证 UI 的稳定性。

应该编写哪种测试类型

我们应该编写哪种测试类型?答案是:都写,根据情况灵活分配。

  • 单元测试:从程序角度出发,对应用程序最小的部分(函数、组件)运行测试的过程,它是从程序员的角度编写的,保证一些方法执行特定的任务,给出特定输入,得到预期的结果。
  • 集成测试:从用户角度出发,对应用中多个模块组织到一起的正确性进行测试。
  • 快照测试:快照测试类似于“找不同”游戏,主要用于 UI 测试。
  • 端到端测试:端到端测试是从用户的角度编写的,基于真实浏览器环境测试用户执行它所期望的工作。

测试金字塔

如果你真的想为你的软件构建自动化测试,你必须知道一个关键的概念:测试金字塔。Mike Cohn 在他的著作《Succeeding with Agile》一书中提出了这个概念。这个比喻非常形象,它让你一眼就知道测试是需要分层的。它还告诉你每一层需要写多少测试。

金字塔模型自下而上分为单元测试、集成测试、UI 测试,之所以是金字塔结构是因为单元测试的成本最低,与之相对,UI 测试的成本最高。所以单元测试写的数量最多,UI 测试写的数量最少。同时需注意的是越是上层的测试,其通过率给开发者带来的信心是越大的。

为了维持金字塔模型的形状,一个健康、快速、可维护的测试组合应该是这样的:许多小而快的单元测试,适当的一些更粗粒度的集成测试,少量高层次的端到端测试。

奖杯模型

奖杯模型摘自 Kent C. Dots 提出的 The Testing Trophy,该模型是笔者比较认可的前端现代化测试模型,模型示意图如下。

参考: