前端自动化测试,你需要知道的事

596 阅读11分钟

为啥要做自动化测试

正题开始之前,我想先唠点我对于测试的个人观点。如果你已经听腻了这些废话只想看点干货可以直接跳过这一部分。作为开发工程师,为什么我们要花时间去写一些用户根本无法感知到的测试脚本呢?

写自动化测试有哪些好处呢?

1. 测试效率

自动化的好处首先自然就是效率了,只要写好一次就可以无限重复的自动化测试在产品迭代的过程中对原有功能的回归测试效率是有质的提升的。

2. 确保产品向下兼容

在进行产品迭代的过程中,自动化测试用例可以很好地检查项目是否向下兼容,代码改动后,可以通过测试结果判断代码的改动是否影响已确定的结果。

3. 长期维护项目的“熵增”

长期维护的项目很难避免“屎山代码”堆积的情况,而适时的重构是减缓这个问题的有效方式,可是重构意味着需要重新捋清楚原有逻辑,很多有志想把代码重构得更优雅的开发者就止步于此。尤其人事变动等各种意外情况下,由于害怕踩坑而选择持续妥协导致情况越来越糟糕。

完善的自动化测试脚本就可以很好地提升开发者的自信,大胆重构减缓项目的“熵增”。

为什么要让开发者来做“测试”?

说了那么多,这“测试”的活儿又跟我开发有什么关系?

  • 首先由于测试工程师手里通常没有源码,不容易感知到代码中容易出错的部分,或者一些微小的细节,导致隐藏的bug影响用户体验。
  • 然后测试脚本通常会依赖一些代码的实现,比如前端UI测试的dom节点选择通常会依赖前端的html代码,由不同人来进行测试无疑会导致沟通成本的上升以及代码健壮性的降低。

在一个全球的权威前端行业调查中,关于**谁在负责团队中的测试?** 的调查结果表示,在88%的case中,开发人员是与QA一样参与到测试过程中的,可见在全球范围内,开发参与测试已经是流行的趋势。

阅读完整报告:tsh.io/state-of-fr…

都有哪些测试类型?

测试金字塔

说到测试类型,相信很多关注过前端测试领域的同学都会想到测试金字塔,它长这样:

测试金字塔最初是由Mike Cohn在《Succeeding with Agile》一书中提出,后来Martin Fowler在其基础上发展成为“Practical Test Pyramid”。由图可见,它由三层组成对应三个测试级别。

  •   单元测试(Unit Tests)
    你会发现单元测试在金字塔中的最底层,这是由于单元测试是低耦合的,主要是对软件中最小的可测试单元进行的检查和验证。因此单元测试是能快速执行且维护简单的。
  •   集成测试(Integration Tests)
    集成测试在金字塔的中层,是一种能很好地兼顾接近用户以及维护简单的一种测试类型。主要关注多个单元组合在一起运行是否符合预期,一般的接口测试,UI组件测试会归属到这个测试类型。
  •   端对端测试,也叫UI测试(E2E Tests, UI Testing)
    端对端测试是模拟真实用户在真实的浏览器环境中交互的一种测试。它能给开发者最强的信心,但是同时执行速度慢、成本更昂贵且脆弱。浏览器差异、时间问题、元素渲染、防刷验证等问题都有可能中断我们的测试,使我们不得不停下来仔细调试,纠结这些问题很容易打击开发者的热情。

测试金字塔鼓励开发者编写多种不同级别的测试,并且层级越高,你编写的测试应该越少。只有低层级无法覆盖的问题才使用更高层级的测试来实现。

测试奖杯(Testing Trophy)

测试奖杯是由 react-testing-library 的作者在其个人博客文章 Static vs Unit vs Integration vs E2E Testing for Frontend Apps 中提出的测试策略模型,其与测试金字塔主要的不同在:

1. 加入了静态测试

一般是指使用ESLint以及Typescript之类的工具对代码在运行之前的检查,比如:

// 使用ESLint可以快速地找到这段代码显而易见的逻辑错误
// 这是一个无限循环
for (var i = 0; i < 10; i--) {
  console.log(i)
}

// 使用Typescript会告诉你'2'是一个字符串类型,不能直接用来跟数字相加
const two = '2'
const result = add(1, two)

代码静态分析也应该是测试流程中不可忽视的组成部分。

2. 提倡更多的集成测试

在测试金字塔中,层级越往上的测试越接近用户,能带来越多的信心,那么为什么我们不干脆全写端对端测试呢?原因是层级越往上的测试同时也是代价越高昂,维护越复杂的测试,投入获得回报的周期也越长。权衡之下,集成测试是能平衡投入(时间)产出(信心)比最好的一种测试类型。

小结

选择一个好的测试策略对刚刚开始起步自动化测试的团队来说尤为重要,因为测试是需要成本的,如果投入了时间无法看到明显的效果,或者在投入时间过程中碰到过多的问题都会很打击开发者的信心,对在团队内推广自动化测试都会是致命的打击,因此测试策略选择上应该根据团队的特点仔细斟酌。

都有哪些工具可以使用?

断言库

在测试中,除了检查要测试的代码是否能正常运行之外,我们还需要对运行的结果进行验证,也就是断言了。社区中有很多开源的断言库,比如expectchai 以及 should.js 等,一般的语法类似:

/** 使用123作为参数测试someFn的逻辑,期望输出为true */
const a = someFn(123);
expect(a).toBe(true);    // 加入输出不为true则会抛出错误。

除此之外,断言库还会提供很多针对不同场景的断言工具,比如方法是否有调用过的断言等:

const fn = jest.fn();
someFn(fn);
// 断言fn没有被调用了
expect(fn).not.toBeCalled();

目前,市面上的断言库都是基于Node.js的assert模块进行封装和扩展的,如下代码是assert模块的工作方式:

 var assert = require('assert'); 
 assert.equal(Math.max(1, 100), 100); 

一旦Math.max(1, 100)的输出不等于100,想回抛出 AssertionError 异常,整个程序将会停止执行。

测试框架

由于assert断言一旦检查失败,就会抛出异常并停止整个应用,这在大规模的断言检查时并不友好,更通用的做法是,记录下抛出的异常并继续执行,最后生成测试报告。这些任务的承担者就是测试框架。

开源社区中的测试框架同样有很多,这里会从支持的功能以及社区流行度的角度简单对比一些主流的测试框架。其中流行度以及社区满意度的数据来源主要是

先来看看对比的结果:

测试框架断言Mock快照覆盖率报告浏览器运行社区流行度社区满意度
Jest默认支持默认支持默认支持默认支持不支持最高93%
Vitest默认支持默认支持默认支持默认支持默认支持94%
Mocha需自行配置需自行配置需自行配置需自行配置支持69%
Jasmine默认支持默认支持需自行配置需自行配置支持一般57%
1. Jest

Jest是Facebook团队开源的测试框架,使用Create React App创建的React应用中会默认内置Jest,Jest提供了舒适的api以及能满足绝大多数需求的开箱即用的功能,这些模式基本已经成为了Web生态系统的标准。是目前社区流行度最高的JavaScript测试框架,缺点是测试用例数量多起来后,运行速度会快速下降,这个问题社区中也有一些方案可以稍作参考:@swc/jestesbuild-jest,这里就不详细介绍了。

2. Mocha

Mocha则是一款以灵活著称的测试框架,支持浏览器环境运行,主体只提供了测试运行的能力,需要自行配置断言库、测试结果报告、代码覆盖率报告等各种各样的工具。一般可以搭配浏览器自动化工具(Puppeteer等)组成一个端对端测试环境。社区使用度颇高,但是维护频率不高,社区满意度呈下滑的趋势。

3. Vitest

Vitest是去年由Vue团队出品的最新的测试框架,由于有Vite的支持,开发体验上几乎是顶级的(主要是快),开发使用上面,Vitest几乎复制了一份Jest的API,支持了Jest拥有的功能且支持在浏览器运行。在Vite项目中可以直接共用原有的项目配置(vite.config.js),在React中使用的配置也不麻烦。缺点是目前还只是0.x.x版本,部分功能还有一些小bug,社区使用度还很低,相信会是一个很有潜力的测试框架。

4. 其他

其他的还有一些更老牌的比如Jasmine、AVA等框架这里就不展开讲了,有兴趣可以继续深入调研。

模拟浏览器环境

测试框架默认是在Node环境下运行的,但是前端的应用是跑在浏览器上的,那测试框架是如何测试前端应用的呢?

jsdom是一个对许多web标准的纯JavaScript逻辑实现,不包含图形渲染部分,主要目的是通过模拟足够多的浏览器功能来实现在Node环境中测试前端应用的需求。

测试框架中Jest会内置jsdom作为依赖,不需要开发者关心,而Mocha需要自己去配置mocha-jsdom,Vitest则支持选择配置jsdom以及happy-dom(一个性能更优的jsdom替代品)来做这部分的事情。

Testing Library

Testing Library是一套简洁而完整的测试工具,帮助开发者实现一个好的测试实践。它鼓励开发者以以用户为中心它的各种设计都基于一个指导思想:

你的测试跟用户使用软件的方式越像,这个测试就能给你越多的自信。

它主要提供了以下方面的能力:

像真实用户一样查找节点

传统的测试框架中定位页面的元素时经常会直接使用CSS Selector通过元素的className来定位元素,但是开发者通常会认为元素的类名只与样式相依赖,这样会导致开发在做一些样式重构时很容易破坏掉原本好的测试用例。而在一些CSS-in-JS、CSS Modules等动态类名的方案中,这种定位方式就更加寸步难行了。

Testing Library鼓励用户用户像真实用户一样查找节点。 比如说,用户在定位一个表单项时会通过定位表单的label或者placeholder来识别这个表单项的含义,使用Testing Library就可以通getByLabelText来模拟真实用户:

import {render, screen} from '@testing-library/react'

render(
  <div>
    <label htmlFor="example">Example</label>
    <input id="example" />
  </div>,
)

// 找出Label为example的表单项
const exampleInput = screen.getByLabelText('Example');    
像真实用户一样交互

想像用户在对一个文本框输入内容时,是不是通常是会先点击一下文本框使其聚焦,然后再输入内容。而传统的测试环境中对文本框输入会直接向对应的input Dom节点派发input事件,这样往往容易忽略掉一些问题。

在Testing Library中可以使用user-event来实现更完整的用户交互:

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('type', () => {
  render(<textarea />)

  // 里面就封装了点击然后输入的事件
  userEvent.type(screen.getByRole('textbox'), 'Hello,{enter}World!')
  expect(screen.getByRole('textbox')).toHaveValue('Hello,\nWorld!')
})
Dom友好的断言方法

Testing Library还扩展了一些断言方法来方便dom上面的一些断言操作。

expect(deleteButton).toHaveClass('extra');    //断言deleteButton有extra类名
expect(nativeButton).toBeInTheDocument();    //断言navtiveButton有渲染到dom树上

Testing Library几乎提供了所有主流的前端框架以及测试框架的完整支持。无论你使用的前端框架是Vue、React还是Svelte等,测试框架是Jest、Mocha还是Cypress等,你都能完整地使用Testing Library所提供的能力来实现以用户为中心的自动化测试脚本,是个人非常推荐的前端测试工具。

浏览器自动化技术

一般的单元测试或者集成测试,使用上面介绍的测试框架、断言库搭配jsdom等浏览器功能模拟即可实现快速执行维护。但是对于需要在真实浏览器环境中运行的端对端测试,我们还需要一些能自动模拟浏览器用户交互的技术。

WebDriver

WebDriver是一个W3C推荐标准,最初来源于Selenium。它定义了一套远程控制接口,可以通过这套接口,开发者可以使用不同的编程语言来实现浏览器的自动化。

WebDriver是一个经典的Client-Server模式。这里Client是指调用WebDriver接口的机器,一般是通过Selenium之类的工具,而Server是指实现了WebDriver标准的浏览器,比如Chrome通过ChromeDriver实现了WebDriver接口,而IE是通过InternetExplorerDriver实现的。

DevTools Protocol

DevTools Protocol是当前主流的浏览器在实现它们的开发者工具(Developer Tools)时定义的协议,我们可以直接通过脚本与这些协议沟通以控制浏览器,上面所说的Chore的Webdriver接口实现——ChromeDriver内部其实也是基于Chrome自己的DevTools Protocol来实现的。

相比WebDriver,DevTools Protocol的实现暂时还没有统一的W3C推荐标准(有在推进中),但是能提供更多的控制能力,比如拦截请求头,模拟网络等一系列我们在开发者工具中能用到的功能。通过这些能力,我们可以在自动化测试中监视网络活动,做一些性能测试等等事情。

当前有实现DevTools Protocol的一些浏览器:

如果没有太多旧浏览器的自动化测试需求(这块即使有提供,用起来也不会太顺畅),一般会推荐更强大的DevTools Protocol。

当然我们并不需要使用这些技术去实现一个浏览器自动化框架,事实上社区中已经有很多强大的成熟的工具可以供我们选择。

常见的端对端测试工具

1. Puppeteer

Puppeteer是Google Chrome 团队官方开发的一个Node库,它基于Chrome DevTools Protocol ****来实现浏览器自动化,因此仅支持Chromium Based浏览器,默认可以在后台跑一个无头Chrome浏览器(指的是不展示用户界面的浏览器),也可以通过配置来切换成完整的浏览器,以供调试。

Puppeteer是一个Node.js库而不是一个专门用来跑测试的框架,因此作为E2E测试环境使用的时候还需要自己搭配测试框架以及断言库等工具,比如上面提到的Mocha+Chai或者Jest。

除了用来做E2E测试之外,Puppeteer还有很多其他的用处:

  • 生成页面的截图或者pdf文件

  • 爬取一个单页应用并生成预渲染内容(SEO优化)

  • 抓取页面的性能数据帮助性能分析

  • ...

2. Selenium

Selenium是一系列UI自动化测试工具的集合,支持多浏览器(包括IE、Safari),支持使用多种语言来编写测试脚本,比如Java、JavaScript、Python等,是一个有十几年历史的老牌自动化测试框架了。Selenium使用的是WebDriver来控制浏览器,事实上WebDriver标准就是由Selenium发起的,在Selenium 4之后,加入了DevTools Protocols支持,加强了浏览器的控制能力。

3. Cypress

Cypress是一个JavaScript端对端测试框架,内部集成了Mocha+Chai等工具,更加开箱即用,也导致不那么灵活,仅支持使用JavaScript进行开发,跨浏览器方面仅支持少数基于Chromium的浏览器,不支持多标签页测试。

它实现浏览器自动化的方式比较独特,没有采取WebDriver或者Devtools protocol的方式来控制浏览器,而是直接在浏览器中运行测试框架,使测试运行过程更加可交互,比如可以对测试的每一步进行调试等,也使直接在测试中引用业务代码直接调用成为可能。

在社区流行度以及满意度上与Puppeteer差不多都获得了不错的成绩,是JavaScript端对端测试框架的主流。

4. PlayWright

PlayWright是Puppeteer的原版开发人员跳槽去了微软开发的自动化测试库。它同时兼容了Chrome、Edge(基于Chromium的)、Firefox以及Safari的DevTools Protocol,从而实现了跨浏览器支持。Playwright 拥有强大的浏览器控制能力,支持多标签页测试,支持模拟主流的移动设备来做移动端测试。但是还比较新,使用率不高,碰到问题时去寻找方案可能会比较麻烦。

写在最后

本文是由我调研收集起的资料结合实践经验概括总结出来,希望你能从本文中获得前端测试领域的一些感性的认知,在此基础上继续调研并开始自己的实践。

对于尚未开始写测试的团队,我的建议是可以先不要求代码覆盖率等指标,先动手写起来,只要你写起来,它就一定能给你带来收益。

参考资料:

[1] Testing Library testing-library.com/

[2] State of frontend tsh.io/state-of-fr…

[3] Testing Pipeline 101 For Frontend Testing www.smashingmagazine.com/2022/02/tes…

[4] Static vs Unit vs Integration vs E2E Testing for Frontend Apps kentcdodds.com/blog/static…

[5] state of js 2021.stateofjs.com/zh-Hans/lib…

[6] What is the difference between WebDriver and DevTool protocol stackoverflow.com/questions/5…