聊聊前端开发的测试

最近在作 Coding 企业版 前端开发时花了不少时间写测试,因而和你们分享一些前端开发中的测试概念与方法。css

前端测试

什么是写测试代码

我理解的写测试实际上是你写一些代码来验证你所谓的能够交付的代码是你所预期的设计,有一些朋友叫他 TDD 也就是测试驱动型的设计,其实究竟是先写代码仍是先写测试,并非最重要的,却是能给你信心这个代码是符合设计的更重要。html

为何要测试,前端须要测试么

这个问题不是这篇分享要和你们聊的,可是做为曾经也有这样疑问的我仍是简单提一下。咱们常常过于自信本身的代码,由于编写的时候已经作过 debug 调试,完过后以为足够了,或者期待下次重构再调整之。结果遇到 bug 没法最快时间肯定问题,别人接手代码也不知道这个模块的设计意图和使用方法,必须跳进去读代码,也不清楚改了一些内容后会不会影响这个模块功能,又得耗时再次 debug 。在弱类型的语言尤为前端开发中尤其明显。那种决定暂时弃之而不顾的的思想很可怕,由于咱们没有听过过勒布朗法则:稍后等于永不。前端

聊聊测试的几种类型

单元测试

从字面意思理解, 写一段代码来测试一个单元。何为单元?其实和编程语言相关,他有多是一个 function,一个 module 一个 package 一个类,固然在 JavaScript 中也颇有可能只是一个 object 。既然如此,那么测试这样的一个小块基本上就是比较孤立,单独验证这个小块的逻辑,一个 function 的输入输出,一个算法的功能和复杂度等等。接下来举几个企业版前端开发中的实际案例。react

咱们使用 jest 做为测试框架(断言库)。jest 会自动搜索全部文件目录下的. spec.js 结尾的文件,而后执行测试。断言库其实还有不少,他们都具有相似 describe , it , expect 些 api。对于一个没有其余依赖的纯函数,例如 redux 中同步 action 或 reducer。 咱们要测的固然就是输入用例而后对应输出是否符合预期ios

it('should return showMore action', () => {
 expect(showMore()).toEqual({  type: ACTION.DEMO_LIST_REMOVE_ITEM,  }); }); 

咱们注意到这样的一个 function 并无 I/O 和 UI 上的依赖,他更有利于作单元测试。其中的 it 接受一个 string 参数,描述一个小测试。另外一个就是测试方法体函数,it 这种测试不能单独使用,通常都包在一个 describe 方法下成为的方法组。那方法体里写什么呢,其实我也能够写成web

if (showMore().type !== ACTION.DEMO_LIST_REMOVE_ITEM) throw 'failed' 

只要抛出异常那么框架就会认为这条测试跑不过。固然 expect 则 api 更加的漂亮,拥有 toEqual toBe、toMapSnap
shot 等判断 api 肯定两个条件之间的关系.
对于纯函数的测试并不难,难的仍是如何把代码写的更可单元测试化,而不要有太多的依赖。算法

集成测试

事实上不少状况小块代码仍是会有函数和 I/O 依赖,好比一些 code 依赖 Ajax 或者 localStorage 或者 IndexedDB ,这样的代码是不能被 united-test 的,因而咱们须要 mock 相应依赖的接口拿到上下文测试咱们的代码,这样的测试叫集成测试。咱们项目中主要依赖了 js-dom 和异步的 action 。下面分别讨论编程

涉及依赖的函数状况 --(异步 action)

事实上不少状况函数仍是会有函数和 I/O 依赖,最典型的就是异步 action 等,他的 I/O 可能会依赖 store.getState(), 自身又会依赖异步中间键。这类使用原生 js 测试起来是比较困难的。咱们思考咱们测试目的,即当咱们触发了一个 action 后它经历了一个圈异步最终 store.getAction 中这个 action 拿到的数据是否和咱们预期一致。既然你们依赖 redux 中 store 的生命周期与 store,因而咱们须要两个工具库 redux-mock-store 和 nock ,因而测试就变成了这样。redux

import configureMockStore from 'redux-mock-store';
import nock from 'nock';
import thunk from 'redux-thunk';
const mockStore = configureMockStore([thunk]);
// 配置mock的store,也让他有咱们相同的middleware
describe('get billings actions', () => {
 afterEach(() => nock.cleanAll());// 每执行完一个测试后清空nock  it('create get all Billings action', () => {  const store = mockStore({  // 以咱们约定的初始state建立store,控制I/O依赖  APP: { enterprise: { key : 'codingcorp' } }  });  const data = [  // 接口返回信息  { ...  },  ];  nock(API_HOST)// 拦截请求返回假定的response  .get(`/api/enterprise/codingcorp/billings`)  .reply(200, { code: 0, data })  return store.dispatch(actions.getAllBillings())  .then(() => {  expect(store.getActions()).toMatchSnapshot();  });  }); }); 
  • 用 nock 来 mock 拦截 http 请求结果,并返回咱们给定的 response。
  • 用 redux-mock-store 来 mock store 的生命周期,须要预先把 middleware 配成和项目一致。
  • desribe 会包含一些生命周期的 api,好比所有测试开始作啥,单个测试结束作啥这类 api。这里每执行完一个测试就清空 nock。
  • 用了 jest 中的 toMatchSnapshot api 判断两个条件一致与否。原先可能要写成expect(store.getActions()).toEqual({data ...});这样,你须要把 equal 里的东西都想具体描写清楚,而 toMatchSnapshot 可在当前目录下生成一个 snapshot 存放这个当前结果,写测试时看一眼结果是预期的就能够 commit。若是改坏了函数就不匹配 snapshot 了。

涉及依赖的函数状况 --(react component)

咱们写的不少 component 是 extends component 的 jsx,测试这类须要一个 mock component 的工具库 Enzyme 。api

 it('should add key with never expire', () => {  ...  挂载咱们的dom  const wrapper = shallow(  <TwoFactorModal  verifyKey={verifyKeySpy}  onVerifySuccess={onVerifySuccessSpy}  />  );  // wrapper的setstate方法  wrapper.setState({  name: 'test',  password: '123',  });  const name = 'new name';  const content = 'new content';  const expiration = '2016-01-01';   wrapper.find('.name').simulate('change', {}, name);  wrapper.find('.content').simulate('change', {}, content);   expect(wrapper.find('.permanentCheck').prop('checked')).toBe(true);  // 此处也可使用toMatchSnapshot  // submit to add  wrapper.find('.submitBtn').simulate('click', e);  return promise.then(() => {  expect(onCheckSuccess).toBeCalledWith({  name,  password,  });  });  }); 

Enzyme 给咱们提供了不少 react-dom 的事件操做与数据获取。
这类 component 的测试通常分为
- Structural Testing 结构测试
主要关心一个界面是否有这些元素
例如咱们有一个界面是
Screen Shot 2017-03-26 at 1.25.15 PM.png
结构化测试将包含:
- 一个 title 包含 “登入到 codingcorp.coding.net”
- 一个副标题包含 “..”
- 两个输入框
- 一个提交按钮
...
比较方便的实现就是利用 jest 的 snapshot 测试方法,先作一个预期生成 snapshot,以后的版本与预期对比。

  • Interaction Testing 交互测试
    好比上述案例触发提交按钮,他应该返回给我用户名和密码,并获得验证结果
    这类通常使用 Enzyme 比较方便

样式测试

UI 的样式测试为了测试咱们的样式是否复合设计稿预期。同时经过样式测试咱们能够感觉当咱们 code 变化带来的 ui 变化,以及他是否符合预期。

inline style

若是样式是 inline style,这类测试其实直接使用 jest 的 Snapshot Testing 最方便,通常在组件库中使用。

CSS

这部分其实属于 E2E 测试中的部分,这里提早讲,主要解决的问题是咱们写出来的 ui 是否符合设计稿的预期。咱们使用 BackstopJS 他的原理是经过对页面的 viewports 和 scenarios 等作配置,利用 web-driver 获取图片,与设计稿或者预期图作 diff,产生报告。

{
// 须要测试的模块元素定义
  "viewports": [
 {  "name": "password", //密码框  "width": 320,  "height": 480  }, ], "scenarios": [  {  "label": "members",  "url": "/member/admin",  "selectors": [ // css选择器  ".member-selector"  ],  "readyEvent": "gmapResponded",  "delay": 100,  "misMatchThreshold" : 1,  "onBeforeScript": "onBefore.js",  "onReadyScript": "onReady.js"  } ], "paths": {  "bitmaps_reference": "backstop_data/bitmaps_reference",  "bitmaps_test": "backstop_data/bitmaps_test",  "html_report": "backstop_data/html_report",  "ci_report": "backstop_data/ci_report" }, "casperFlags": [], "engine": "slimerjs", "report": ["browser"], "debug": false } 

最后会得出相似这样的报告
Screen Shot 2017-03-26 at 11.41.38 AM.png

E2E 测试

E2E 测试是在实际生产环境测试整个 app,一般来讲这部分工做会让测试人工作,并在实体环境跑,就像用户实际在操做同样。靠人工作遇到项目逻辑比较复杂,则须要每个版本都要测不少逻辑,担忧提交一个影响了其余部分。其实也有比较好的自动化跑脚本方案能帮助测试,咱们使用 selenium-webdriver 工具配合 async await 进行自动化 E2E 测试。

const {prepareDriver, cleanupDriver} = require('../utils/browser-automation')

//...
describe('member', function () {
  let driver
  ...
  before(async () => {
 driver = await prepareDriver() }) after(() => cleanupDriver(driver)) it('should work', async function () { const submitBtn = await driver.findElement(By.css('.submitBtn'))  await driver.get('http://localhost:4000')  await retry(async () => {  const displayElement = await driver.findElement(By.css('.display'))  const displayText = await displayElement.getText()  expect(displayText).to.equal('0') })  await submitBtn.click() }) 

selenium-webdriver 提供了不少浏览器的操做以及对元素对查找方法,以及元素内容的获取方法,好比这里的 By.css 选择器。
有时候用户端的设备很不一致,须要在不一样设备上的匹配,因而咱们能够用 selenium-webdriver 搭配 sourcelab 的设备墙进行
sourcelab.png

测试覆盖率与代码变异测试

测试覆盖率表达本次测试有有多少比例的语句,函数分支没有被测到。固然绝对数字做为代码质量依据并无什么意义,由于它是根据咱们写的测试来的。却是学习为何有些代码没有被覆盖到,以及为何有些代码变了测试却没有失败。颇有意义。咱们在 jestconfig 中配置完目标数据后,每次他会检测咱们的测试覆盖率并给咱们报告
Screen Shot 2017-03-26 at 12.25.30 PM.png

Function Coverage 函数覆盖

顾名思义,就是指这个函数是否被测试代码调用了。如下面的代码为例
,对函数 exchange 要作到覆盖,只要一个测试——如 expect(exchange(2, 2)) 就能够了。若是连函数覆盖都达不到,那这个函数是否真的须要。

let z = 0
  if (x>0 && y>0) {
 z=x } return z } 

Line Coverage 语句覆盖

仍是前面那个 exchange 例子,他检测的是某一行代码是否被测试覆盖了,一样 选择用例 2,2 也能覆盖它,可是若是变成 2, -1 就不行了。一般这种状况是因为一些分支语句致使的,由于相应的问题就是 “那行代码(以及它所对应的分支)须要吗?

Decision Coverage 决策覆盖

它是指每个逻辑分支是否被测试覆盖了,有一个 if 的真和假通常就要两组用例,至少测一组 true 一组 false

Condifiton Coverage 条件覆盖

它是指分支中的每一个条件是否被测试覆盖了,像前面那个 exchange 例子,要达到所有条件覆盖,测试用例就须要四个,即 x 和 y 四种状况,若是测不到就要思考是否不须要某个分支呢

代码变异测试

说到这里从新提一下 jest 的 toMatchSnapshot 实践,他对指望的表达并非写一个指望值和实际作匹配,而是生成一个快照让咱们以后的每次变异代码和它匹配, jest--watch 的实时测试变更的代码更方便作这个事。
这里所谓的变异是指修改一处代码来改变代码的行为,检查测试是否由于这个代码的变异而失败,若是有失败则说明这个变异被消灭,此时的测试自己行为是符合预期。否则变异存活则测试不到位。
平时用到比较多的变异方法是:
条件边界变异、反向条件变异、数学运算变异、增量运算变异、负值翻转变异等

小结

养成写测试的好习惯能避免不少问题,极大的提高效率,避免重复 debug。在前端开发中因为语言自己对写法限制比较弱,测试保障很是重要,既让本身对代码有信心也让别人更容易理解你设计的每个模块用意。在写代码的时候就要从可测试如何测试的角度思考,尽可能每一行代码都是有用且符合预期的。