基于jest和puppeteer的前端自动化测试实战

前端测试现状

常常听到后端同窗说“单元测试”,前端写过测试用例的有多少?答案是:并很少,为何呢?两个主要缘由html

一、前端属于GUI软件,浏览器众多,兼容问题让人头大,用户量有必定规模的浏览器包括:前端

  IE八、IE九、IE十、IE十一、chrome、FireFox、360浏览器、搜狗浏览器、QQ浏览器……node

要在这么多浏览器上作几轮测试并不容易react

二、前端界面变化快,不少时候界面比测试脚本迭代的更快,测试跟不上脚步,投入产出不成正比git

以上两点致使前端测试不受重视,不少前端开发者可能工做数年仍未写过单元测试github

英国的一个前端开发者作了一项前端测试工具调查发现,目前仍有43%的前端开发者没有作过任何前端测试,这是现状web

 

 

该不应写前端测试,仍是得视项目状况而定,通常标准的开源项目都会作单元测试,因此有必要了解一下前端测试大概是个什么东西chrome


 

分类

前面一直说的是前端测试而不是单元测试,是由于前端不一样于后端,前端是有界面的,测试应该分为单元测试和集成测试npm

所谓单元测试,就是测试一个函数或某个代码片断,经过模拟输入确保输出符合预期后端

实例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就是干这个的,它有不少 匹配方法,实例1中的

expect(sum(1,1)).toBe(2);

 意思就是断言函数sum执行的结果等于2,其它匹配方法包括但不限于:

  • 判断某个变量是否认义:.toBeDefined();
  • 比较某个值是否大于指定数字:.toBeGreaterThan(number);
  • 检查对象length属性是否等于指定值:.toHaveLength(number);
  • ……

 

mock定时器 

业务代码中常常会用到定时器,包括setTimeout、setInterval,在作单元测试的时候,若是傻傻地等定时器一秒一秒走那就很浪费时间,你们都是一秒钟几十万上下的人,哪怕几秒钟也不会浪费,jest的mock功能,能够模拟定时器执行,有4个重要的API必须了解一下:

  1. jest.useFakeTimers() 声明在当前测试文件中使用模拟定时器,声明后,能够直接用expect(setTimeout).toHaveBeenCalledTimes(1)判判定时器调用的次数
  2. jest.runAllTimers() 当即执行全部定时器 
  3. jest.runOnlyPendingTimers() 当即执行挂起的定时器
  4. jest.advanceTimersByTime(msToRun) 提早msToTun毫秒执行定时器

第1个API须要注意,仅仅声明jest.useFakeTimers(),定时器回调的代码并不会执行,第二、三、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 集成测试 

两种测试方法各有优缺点,具体用哪一种视项目具体状况而定

相关文章
相关标签/搜索