前端测试现状
经常听到后端同学说 "单元测试", 前端写过测试用例的有多少? 答案是: 并不多, 为什么呢? 两个主要原因
1, 前端属于 GUI 软件, 浏览器众多, 兼容问题让人头大, 用户量有一定规模的浏览器包括:
IE8,IE9,IE10,IE11,chrome,FireFox,360 浏览器, 搜狗浏览器, QQ 浏览器......
要在这么多浏览器上做几轮测试并不容易
2, 前端界面变化快, 很多时候界面比测试脚本迭代的更快, 测试跟不上脚步, 投入产出不成正比
以上两点导致前端测试不受重视, 很多前端开发者可能工作数年仍未写过单元测试, 这是现状
该不该写前端测试, 还是得视项目情况而定, 一般标准的开源项目都会做单元测试, 所以有必要了解一下前端测试大概是个什么东西
分类
前面一直说的是前端测试而不是单元测试, 是因为前端不同于后端, 前端是有界面的, 测试应该分为单元测试和集成测试
所谓单元测试, 就是测试一个函数或某个代码片段, 通过模拟输入确保输出符合预期
实例 1: 以下是一个完整的测试用例, 用来测试函数 sum 是否按预期的计算两个数字之和
- const sum = (a,b) => {
- return a+b;
- }
- describe('分组测试描述',() => {
- test('test 1+1', () => {
- expect(1 + 1).toBe(2);
- });
- })
解释一下两个关键字:
describe, 作用是将 test 分组, 影响 beforeEach/afterEach/beforeAll/afterAll 四个方法的作用域, 它有两个参数
第一个参数就是分组描述, 描述这个分组是干嘛的
第二个参数是个回调函数, 内部可以有多个 test,test 的作用是声明一个测试
test, 作用就是声明一个测试, 有三个参数
第一个同样是描述, 描述测试内容
第二个也是回调, 内部为详细测试内容
第三个是测试超时时间, 默认为 5s 钟, 做单元测试一般都是足够的, 集成测试一般都是不够的, 可以用 jest.setTimeout(timeout) 方法修改所有 test 默认超时时间
集成测试, 测的是一个功能模块, 比如用户注册功能, 集成测试又包括 UI 测试, UI 测试用于确保页面正常渲染
集成测试完全是用测试脚本去模拟用户操作, 比如打开浏览器, 点击注册链接, 输入用户名密码, 点击注册
UI 测试怎么确保页面正常渲染?
两种方式: 像素级对比和快照
像素及对比, 就是首先人肉确认页面渲染正常, 执行脚本对页面截个图, 下次利用测试脚本截个图跟上次的截图的每个像素自动进行对比, 如果每个像素都一样, 那么测试通过
快照, 这里的快照不是截图的意思, 而是将页面渲染后的 DOM 结构生成一个序列化的文本, 下次再次生成一个序列化的 DOM 文本与之对比, 如果内容完全一样, 测试通过, 做快照测试, 必须保证多次测试输出快照总是一致的, 然而在 react 中, model 经常变化, 这时就要用 mock 模拟函数返回固定数据确保 model 不变, mock 功能在下文有介绍
主流库
流行的单元测试包括 jest,mocha,jasmine,......
流行的集成测试库包括 puppeteer,casperJS,PhantomJS,......
jest 的特点是零配置, 即时反馈, 它所有测试用例默认是并行执行的, 速度快, 它也可以配置成串行, 在调试时比较有用, jest 每个测试用例文件都是一个沙箱, 在单个测试文件内部定义或修改全局变量, 不会影响其它测试文件, jest 由 Facebook 团队维护, 对 React 友好, 适合大型项目
mocha 是一个精简而灵活的单元测试框架, 它本身没有包含断言库和 mock(模拟) 功能, 需要自行引入其它库, 而 jest 和 jasmine 都自带断言库和 mock 功能, 什么是 mock, 后面会介绍
puppeteer 是个神器, 它并不仅仅可以做自动化集成测试, 它本身是个 node 库, 自带 chromuium 浏览器 (所以 npm 安装它比较慢), 它提供了一些高级 API 通过 DevTools 协议控制 headless chrome 或 chromuium, 它也可以配置为使用有界面版的 chrome, 既然是浏览器, chrome 能做到的它基本都能做到, chrome 做不到的, 它也能做到, 用 puppeteer 做集成测试, 测试用例是真正在真实的浏览器上执行的, 下面几点都是它所擅长的
生成页面屏幕截图或 pdf
自动提交表单, 做 UI 测试, 模拟键盘输入, 鼠标操作等
创建一个最新的自动化测试环境, 用最新的 JavaScript 和浏览器功能, 直接在最新的 chrome 中做测试
捕获你网站的时间线跟踪, 以帮助诊断性能问题
casperJS 是一个基于 PhantomJS 的库, 它封装了 PhantomJS 的 API 使它更容易使用, PhantomJS 内置了 webkit 的内核, 测试用例并不是跑在真正的浏览器上面
本文的重点是 jest 和 puppeteer, 下面是实例和 API 都是基于这两者
Setup
如果在执行 jest 测试用例之前需要做一些配置, 在用例执行完做一些清除操作, 那你需要了解下面 4 个 API
beforeEach(callback)
在每个 test 用例执行前执行回调 callback, 在单个测试文件内, 它对每个 test 都有效, 如果它放在 describe 内部, 那么它只对 describe 内部的 test 用例有效, 上面讲过, describe 内部可以有多个 test
afterEach(callback)
在每个 test 用例执行后执行回调 callback, 作用域同 beforeEach
beforeAll(callback)
在所有 test 用例执行前执行回调 callback
afterAll(callback)
在所有 test 用例执行后执行回调 callback
断言
在编写测试时, 您经常需要检查值是否符合某些条件. Expect 就是干这个的, 它有很多 匹配方法 https://facebook.github.io/jest/docs/en/expect.html , 实例 1 中的
expect(sum(1,1)).toBe(2);
意思就是断言函数 sum 执行的结果等于 2, 其它匹配方法包括但不限于:
判断某个变量是否定义:.toBeDefined();
比较某个值是否大于指定数字:.
toBeGreaterThan(number);
检查对象 length 属性是否等于指定值:.
- toHaveLength(number);
- ......
mock 定时器
业务代码中经常会用到定时器, 包括 setTimeout,setInterval, 在做单元测试的时候, 如果傻傻地等定时器一秒一秒走那就很浪费时间, 大家都是一秒钟几十万上下的人, 哪怕几秒钟也不会浪费, jest 的 mock 功能, 可以模拟定时器执行, 有 4 个重要的 API 必须了解一下:
jest.useFakeTimers() 声明在当前测试文件中使用模拟定时器, 声明后, 可以直接用 expect(setTimeout).toHaveBeenCalledTimes(1) 判断定时器调用的次数
jest.runAllTimers() 立即执行所有定时器
jest.runOnlyPendingTimers() 立即执行挂起的定时器
jest.advanceTimersByTime(msToRun) 提前 msToTun 毫秒执行定时器
第 1 个 API 需要注意, 仅仅声明 jest.useFakeTimers(), 定时器回调的代码并不会执行, 第 2,3,4 个 API 都会真正执行定时器回调代码;
jest.runOnlyPendingTimers() 执行挂起的定时器是什么意思? 其实就是即将要执行的那一个定时器, 下面这段代码, 会调用两次 setTimeout, 第一次是 jest.useFakeTimers() 触发的, 第二次是 jest.runOnlyPendingTimers() 触发的
- function timeout() {
- setTimeout(() => {
- console.count('count');
- timeout();
- }, 10000);
- }
- jest.useFakeTimers();
- test('useFakeTimers', () => {
- timeout();
- jest.runOnlyPendingTimers();
- expect(setTimeout).toHaveBeenCalledTimes(2);
- });
如果把上一段测试用例的 jest.runOnlyPendingTimers() 换成 jest.runAllTimers() 会进入死循环
mock 函数
手动实现了一个 forEach 函数, 要测试它是否按预期执行回调, 这里模拟了一个回调函数 mockCallback, 模拟函数的好处是可以获取每次调用它的参数和它的执行次数, 在项目中可以模拟请求返回指定数据而无需访问服务器
- function forEach(items, callback) {
- for (let index = 0; index <items.length; index++) {
- callback(items[index]);
- }
- }
- test('test forEach', () => {
- const mockCallback = jest.fn();
- forEach([0, 1], mockCallback);
- // The mock function is called twice
- expect(mockCallback.mock.calls.length).toBe(2);
- // The first argument of the first call to the function was 0
- expect(mockCallback.mock.calls[0][0]).toBe(0);
- // The first argument of the second call to the function was 1
- expect(mockCallback.mock.calls[1][0]).toBe(1);
- });
异步
测试脚本中可能包含异步操作, 如果不用异步方式写 test,test 执行到最后一行就认为测试完成, 很可能测试失败
方式一: done 回调, 传入参数 done, 异步操作执行完后执行 done()
- // done
- test('async test', done => {
- function callback(data) {
- expect(data).toBe('xx');
- done();
- }
- fetchData(callback);
- });
方式二: 返回 promise,test 会等 promise 执行完才跳出
- // return promise
- test('async test', () => {
- // 判断当前测试有一个断言被执行
- expect.assertions(1);
- return fetchData().then(data => {
- expect(data).toBe('xx');
- });
- });
方式三:.resolves/.rejects, 同样必须 return promise
- test('works with resolves', () => {
- expect.assertions(1);
- return expect(user.getUserName(5)).resolves.toEqual('xx');
- });
方式四: ES8 的 async/await, 可以和. resolves/.rejects 混合使用
- // async/await can be used.
- it('works with async/await', async () => {
- expect.assertions(1);
- const data = await user.getUserName(4);
- expect(data).toEqual('xx');
- });
- // async/await can also be used with `.resolves`.
- it('works with async/await and resolves', async () => {
- expect.assertions(1);
- await expect(user.getUserName(5)).resolves.toEqual('xx');
- });
关于 describe 还有两个重要重要的方法应该了解下
describe.only(name, fn)
只执行该 describe, 其它 describe 会被忽略
describe.skip(name, fn)
和. only 相反, 只跳过该 describe, 在调试时很有用
puppeteer 常用的几个 API 也了解一下
puppeteer.launch() 实例化一个浏览器
browser.newPage(url) 打开新页面
page.goto(url) 跳转到 url
page.$(selector) 选择页面元素, 返回的是元素句柄 (ElementHandle), 不是真实 DOM 节点, selector 底层实现用的就是 document.querySelector
page.$$(selector) 同上, selector 底层实现用的就是 document.querySelectorAll, 返回多个句柄
page.$eval(selector, pageFunction[, ...args]) 同上, 返回的是 pageFunction 的返回值, 在 pageFunction 内可以获取到真实 DOM 节点, 如获取元素 ID,page.$eval('div', divs => divs.id);
page.$$eval(selector, pageFunction[, ...args]) 同上, selector 底层实现用的就是 document.querySelectorAll
page.click(selector[, options]) 点击指定元素
page.type(selector, text[, options]) 改变元素的值, 如果是 react, 会同时改变 model 层数据, 就像真实用户输入
单元测试 VS 集成测试
两种测试方法各有优缺点, 具体用哪种视项目具体情况而定
来源: https://www.cnblogs.com/wangmeijian/p/9080188.html