基础
自动化测试(automated testing)是利用计算机程序检查软件是否运行正常的测试方法
相比手动测试而言节省了大量时间以及提高了程序的稳定性。
可使用许多种不同的方法来编写自动化测试脚本。
- 可以编写通过浏览器自动执行的程序
- 可以直接调用源代码里的函数
-
测试分类
前端开发最常见的测试主要是以下几种:
单元测试:验证独立的单元是否正常工作
- 集成测试:验证多个单元协同工作
- 端到端测试:从用户角度以机器的方式在真实浏览器环境验证应用交互
- 快照测试:验证程序的 UI 变化
单元测试
单元测试是对应用程序最小的部分(单元)运行测试的过程。通常,测试的单元是函数,但在前端应用中,组件也是被测单元。
单元测试可以单独调用源代码中的函数并断言其行为是否正确。
由于单元测试是独立的,所以无法保证多个单元运行到一起是否正确
Mocha 跟 Jest 是目前最火的两个单元测试框架,基本上目前前端单元测试就在这两个库之间选了。总的来说就是 Jest 功能齐全,配置方便,Mocha 灵活自由,自由配置。 两者功能覆盖范围粗略可以表示为:Jest === Mocha + Chai + Sinon + mockserver + istanbul
集成测试
人们定义集成测试的方式并不相同,尤其是对于前端。有些人认为在浏览器环境上运行的测试是集成测试;有些人认为对具有模块依赖性的单元进行的任何测试都是集成测试;也有些人认为任何完全渲染的组件测试都是集成测试。
优点:
- 由于是从用户使用角度出发,更容易获得软件使用过程中的正确性
- 集成测试相对于写了软件的说明文档
- 由于不关注底层代码实现细节,所以更有利于快速重构
- 相比单元测试,集成测试的开发速度要更快一些
缺点:
- 测试失败的时候无法快速定位问题
- 代码覆盖率较低
-
端到端测试(E2E)
E2E(end to end)端到端测试是最直观可以理解的测试类型。在前端应用程序中,端到端测试可以从用户的视角通过浏览器自动检查应用程序是否正常工作。
优点: 真实的测试环境,更容易获得程序的信心
缺点:
- 首先,端到端测试运行不够快。启动浏览器需要占用几秒钟,网站响应速度又慢。通常一套端到端测试需要 30 分钟的运行时间。如果应用程序完全依赖于端到端测试,那么测试套件将需要数小时的运行时间。
- 端到端测试的另一个问题是调试起来比较困难。要调试端到端测试,需要打开浏览器并逐步完成用户操作以重现 bug。本地运行这个调试过程就已经够糟糕了,如果测试是在持续集成服务器上失败而不是本地计算机上失败,那么整个调试过程会变得更加糟糕。
端到端测试框架:Cypress
快照测试
快照测试类似于“找不同”游戏。快照测试会给运行中的应用程序拍一张图片,并将其与以前保存的图片进行比较。如果图像不同,则测试失败。这种测试方法对确保应用程序代码变更后是否仍然可以正确渲染很有帮助。
测试金字塔
- 单元测试:从程序角度出发,对应用程序最小的部分(函数、组件)运行测试的过程,它是从程序员的角度编写的,保证一些方法执行特定的任务,给出特定输入,得到预期的结果。
- 集成测试:从用户角度出发,对应用中多个模块组织到一起的正确性进行测试。
- 快照测试:快照测试类似于“找不同”游戏,主要用于 UI 测试。
- 端到端测试:端到端测试是从用户的角度编写的,基于真实浏览器环境测试用户执行它所期望的工作。
金字塔模型自下而上分为单元测试、集成测试、UI 测试, 之所以是金字塔结构是因为单元测试的成本最低, 与之相对, UI 测试的成本最高。所以单元测试写的数量最多, UI 测试写的数量最少。同时需注意的是越是上层的测试, 其通过率给开发者带来的信心是越大的。
奖杯模型摘自 Kent C. Dots 提出的 The Testing Trophy, 该模型是笔者比较认可的前端现代化测试模型, 模型示意图如下。
奖杯模型中自下而上分为静态测试、单元测试、集成测试、e2e 测试, 它们的职责大致如下:
- 静态测试:在编写代码逻辑阶段时进行报错提示。(代表库: ESLint、Flow、TypeScript)
- 单元测试:在奖杯模型中, 单元测试的职责是对一些边界情况或者特定的算法进行测试。(代表库: Jest、Mocha)
- 集成测试:模拟用户的行为进行测试,对网络请求、获取数据库的数据等依赖第三方环境的行为进行 Mock。(代表库: Jest、react-testing-library、Vue Testing Library 等)
- e2e 测试:模拟用户在真实环境上操作行为(包括网络请求、获取数据库数据等)的测试。(代表库: Cypress)
越是上层的测试给开发者带来的自信是越大的,与此同时,越是下层的测试测试的效率是越高的。奖杯模型综合考虑了这两点因素, 可以看到其在集成测试中的占比是最高的。
为了维持奖杯模型的形状,一个健康、快速、可维护的测试组合应该是这样的:
- 在底层为应用配置静态测试,比如使用 ESLint 约束代码规范、使用 TypeScript 增强类型定义
- 为应用中的特定算法或是工具函数编写小而快的单元测试
- 写许多模拟真实用户行为的集成测试,增强应用构建信心
- 为稳定的组件编写快照测试
- 为应用核心业务流程编写少量的高层次端到端测试
下面是针对不同的应用场景为了一些个人建议:
- 如果你是开发纯函数库,建议写更多的单元测试 + 少量的集成测试
- 如果你是开发组件库,建议写更多的单元测试、为每个组件编写快照测试、写少量的集成测试 + 端到端测试
- 如果你是开发业务系统,建议写更多的集成测试、为工具类库、算法写单元测试、写少量的端到端测试
测试覆盖率
测试覆盖率(test coverage)是衡量软件测试完整性的一个重要指标。掌握测试覆盖率数据,有利于客观认识软件质量,正确了解测试状态,有效改进测试工作。代码覆盖率
最著名的测试覆盖率就是代码覆盖率。这是一种面向软件开发和实现的定义。它关注的是在执行测试用例时,有哪些软件代码被执行到了,有哪些软件代码没有被执行到。被执行的代码数量与代码总数量之间的比值,就是代码覆盖率。
这里,根据代码粒度的不同,代码覆盖率可以进一步分为四个测量维度。它们形式各异,但本质是相同的。
- 行覆盖率(line coverage):是否每一行都执行了?
- 函数覆盖率(function coverage):是否每个函数都调用了?
- 分支覆盖率(branch coverage):是否每个if代码块都执行了?
- 语句覆盖率(statement coverage):是否每个语句都执行了?
如何度量代码覆盖率呢?一般可以通过第三方工具完成,比如 Jest 自带了测试覆盖率统计。
这些度量工具有个特点,那就是它们一般只适用于白盒测试,尤其是单元测试。对于黑盒测试(例如功能测试/系统测试)来说,度量它们的代码覆盖率则相对困难多了。
需求覆盖率
对于黑盒测试,例如功能测试/集成测试/系统测试等来说,测试用例通常是基于软件需求而不是软件实现所设计的。因此,度量这类测试完整性的手段一般是需求覆盖率,即测试所覆盖的需求数量与总需求数量的比值。
我认为测试覆盖率还是要和测试成本结合起来,比如一个不会经常变的公共方法就尽可能的将测试覆盖率做到趋于 100%。而对于一个完整项目,我建议前期先做最短的时间覆盖 80% 的测试用例,后期再慢慢完善。
大多数情况下,将 100% 代码覆盖率作为目标并没有意义。当然,如果你在开发一个极其重要的支付应用,存在的 Bug 可能会导致数百万美元的损失,那么 100% 代码覆盖率对你是有用的。
测试开发方式
TDD 的优点:
- 保证代码质量,因为先编写测试,所以可能出现的问题都被提前发现了;
- 促进开发人员思考,有利于程序的模块设计;
- 测试覆盖率高,因为后编写代码,因此测试用例基本都能照顾到;
TDD 的缺点:
- 代码量增多,大多数情况下测试代码是功能代码的两倍甚至更多;
- 业务耦合度高,测试用例中使用了业务中一些模拟的数据,当业务代码变更的时候,要去重新组织测试用例;
- 关注点过于独立,由于单元测试只关注这一个单元的健康状况,无法保证多个单元组成的整体是否正常;
个人理解在前端应用实际开发过程中 TDD 更适合开发纯函数库、比如 Lodash、Vue、React 等。
BDD
BDD(Behavior-driven development)行为驱动开发,是测试驱动开发延伸出来的一种敏捷软件开发技术。
TDD 最大一个问题是在于开发人员最终做出来的东西和实际功能需求可能相偏离,为了解决这一问题有人发明了 BDD。
BDD 的开发流程大致是:
1、开发人员和非开发人员一起讨论确认需求
2、以一种自动化的方式将需求建立起来,并确认是否一致
3、最后,实现每个文档示例描述的行为,并从自动化测试开始以指导代码的开发
这样做的想法是使每个更改较小并快速迭代,每次需要更多信息时都将其上移。每次您自动化并实现一个新示例时,便为系统添加了一些有价值的内容,并准备响应反馈。
理想中的 BDD 解决方案最流行的是 Cucumber。它的协作流程是这样:
1、开发人员与产品、测试、客户等人员沟通确认需求
2、使用统一的 Gherkin 语法将功能需求转换为需求文档
3、开发人员根据 Gherkin 编写测试用例
4、编写代码使测试通过
5、新增功能重复上述步骤
BDD 注重的是产品功能,可能无法保证很好的代码质量和测试覆盖率,所以还有人提出一种方案就是 BDD + TDD
- BDD
- 需求分析
- 描述需求定义文档
- 编写集成测试用例
- TDD
- 编写单元测试用例
- 编写代码使单元测试通过
- 重构优化
- 编写代码使集成测试通过
- 增加功能重复上述步骤
个人理解也可以把 BDD 看作是在需求与 TDD 之间架起一座桥梁,它将需求进一步场景化,更具体的描述系统应该满足哪些行为和场景,让 TDD 的输入更优雅、更可靠。
还有一种更轻量的 BDD 方案就是以集成测试为主的开发方案。
- 需求分析
- 编写集成测试用例
- 运行测试
- 代码实现使测试通过
- 重构优化
- 增加功能重复上述步骤
TDDvsBDD
功能 | TDD | BDD |
---|---|---|
定义 | 测试驱动开发 | 行为驱动开发 |
思想 | 从代码角度出发,以完成高质量代码为目的。 | 从用户角度出来,为完成功能需求为目的。 |
开发流程 | - 需求分析 - 编写单元测试 - 运行测试 - 编写代码 - 运行测试 - 重构优化 - 重复上述步骤 |
- 开发人员与产品、测试、客户等人员沟通并确定功能需求 - 使用统一格式文档描述功能需求文档 - 根据功能需求文档建立测试用例 - 运行测试 - 编写代码实现功能 - 运行测试使通过 - 重构优化 - 重复上述步骤 |
代码覆盖率 | 高 | 一般 |
软件安全感 | 一般 | 高 |
测试类型 | 单元测试 | 集成测试 |
代码解耦 | 一般 | 高 |
开发效率 | 一般 | 高 |
代码质量 | 高 | 一般 |
测试代码量 | 高 | 低 |
个人推荐:
- 建议开发功能函数库使用 TDD 方案;
- 建议开发业务系统使用 BDD 方案
前端自动化测试的权衡利弊
当我们开始编写自动化测试时,可能想要测试所有的东西。因为我亲身经历了未经测试的应用程序带来的痛苦。我不想再体验同样的经历,但很快我又学到了另一课——测试会减缓开发速度。
在编写测试时,请务必牢记编写测试的目的。通常,测试的目的是为了节省时间。如果你正在进行的项目是稳定的并且会长期开发,那么测试是可以带来收益的。
但是如果测试编写与维护的时间长于它们可以节省的时间,那么你根本不应该编写测试。当然,在编写代码之前你很难知道通过测试可以节省多少时间,你会随着时间的推移去了解。但是,假设你正在一个短期项目中创建原型,或者是在一个创业公司迭代一个想法,那你可能不会从编写测试中获得收益。
凡事都有两面性,软件测试也不是银弹,好处虽然明显,却并不是所有的项目都值得引入测试框架,毕竟维护测试用例也是需要成本的。对于一些需求频繁变更、复用性较低的内容,比如活动页面,让开发专门抽出人力来写测试用例确实得不偿失。
而适合引入测试场景大概有这么几个:
- 需要长期维护的项目。它们需要测试来保障代码可维护性、功能的稳定性
- 较为稳定的项目、或项目中较为稳定的部分。给它们写测试用例,维护成本低
被多次复用的部分,比如一些通用组件和库函数。因为多处复用,更要保障质量
测试确实会带给你相当多的好处,但不是立刻体验出来。正如买重疾保险,交了很多保费,没病没痛,十几年甚至几十年都用不上,最好就是一辈子用不上理赔,身体健康最重要。测试也一样,写了可以买个放心,对代码的一种保障,有 bug 尽快测出来,没 bug 就最好,但不能说“写那么多测试,结果测不出 bug,浪费时间”吧。
参考
https://www.yuque.com/lipengzhou/frontend-testing/gg965i#IpCyc