前端单元测试探究

本文已宣布在稀土掘金

转载请说明原文链接:https://github.com/ecmadao/Co…

虽然许多公司有本身的测试部门,而且前端开辟大多不触及测试环节,但鉴于现在前端范畴的疾速生长,其触及面愈来愈广,前端开辟者们必定不能止步于现在的状况。我以为学会编写优越的测试,不单单议有利于本身整顿需求、搜检代码,更是一个优异开辟者的表现。

起首不能不引荐两篇文章:

前端自动化测试探究

测试驱动开辟(TDD)引见中的误区

Intro

单元测试究竟是什么?

须要接见数据库的测试不是单元测试

须要接见收集的测试不是单元测试

须要接见文件体系的测试不是单元测试

— 修正代码的艺术

我们在单元测试中应当防止什么?

  • 太多的条件逻辑

  • 组织函数中做了太多事变

  • too many全局变量

  • too many静态要领

  • 无关逻辑

  • 过量外部依靠

TDD(Test-driven development)

测试驱动开辟(TDD),其基本思路是经由历程测试来推进悉数开辟的举行。

  • 单元测试的主要目的不是为了能够编写出大覆蓋率的悉数经由历程的测试代码,而是须要从运用者(挪用者)的角度动身,尝试函数逻辑的种种可能性,进而辅助性加强代码质量

  • 测试是手腕而不是目的。测试的主要目的不是证实代码准确,而是协助发明毛病,包含初级的毛病

  • 测试要快。疾速运转、疾速编写

  • 测试代码坚持简约

  • 不会疏忽失利的测试。一旦团队最先接收1个测试的构建失利,那末他们渐渐地顺应2、3、4或许更多的失利。在这类状况下,测试集就不复兴作用

IMPORTANT

  • 肯定不能误解了TDD的中心目的!

  • 测试不是为了覆蓋率和准确率

  • 而是作为实例,通知开辟职员要编写什么代码

  • 红灯(代码还不完美,测试挂)-> 绿灯(编写代码,测试经由历程)-> 重构(优化代码并保证测试经由历程)

大抵历程

  1. 需求剖析,思索完成。斟酌怎样“运用”产物代码,是一个实例要领照样一个类要领,是从组织函数传参照样从要领挪用传参,要领的定名,返回值等。这时候实在就是在做设想,而且设想以代码来表现。此时测试为红

  2. 完成代码让测试为绿

  3. 重构,然后反复测试

  4. 终究相符一切请求:

    • 每一个观点都被清楚的表达

    • Not Repeat Self

    • 没有过剩的东西

    • 经由历程测试

BDD(Behavior-driven development)

行动驱动开辟(BDD),重点是经由历程与好处相干者的议论,获得对预期的软件行动的清醒认识,其重点在于沟通

大抵历程

  1. 从营业的角度定义详细的,以及可权衡的目的

  2. 找到一种能够到达设定目的的、对营业最主要的那些功用的要领

  3. 然后像故事一样形貌出一个个详细可执行的行动。其形貌要领基于一些通用辞汇,这些辞汇具有准确无误的表达能力和一致的寄义。比方,expect, should, assert

  4. 寻觅适宜语言及要领,对行动举行完成

  5. 测试职员磨练产物运转效果是不是相符预期行动。最大水平的交支付相符用户希冀的产物,防止表达不一致带来的题目

测试的分类 & 测试东西

分类

  • API/Func UnitTest

    • 测试不常变化的函数逻辑

    • 测试前后端API接口

  • UI UnitTest

    • 页面自动截图

    • 页面DOM元素搜检

    • 跑通交互流程

东西

mocha + chai的API/Func UnitTest

mocha是一套前端测试东西,我们能够拿它和其他测试东西搭配。

而chai则是BDD/TDD测试断言库,供应诸如expect如许的测试语法

initial

下面两篇文章值得一看:

Testing in ES6 with Mocha and Babel 6

Using Babel

setup
$ npm i mocha --save-dev
$ npm i chai --save-dev
Use with es6

babel 6+

$ npm install --save-dev babel-register
$ npm install babel-preset-es2015 --save-dev
// package.json
{
  "scripts": {
    "test": "./node_modules/mocha/bin/mocha --compilers js:babel-register"
  },
  "babel": {
    "presets": [
      "es2015"
    ]
  }
}

babel 5+

$ npm install --save-dev babel-core
// package.json
{
  "scripts": {
    "test": "./node_modules/mocha/bin/mocha --compilers js:babel-core/register"
  }
}
Use with coffeescript
$ npm install --save coffee-script
{
  "scripts": {
    "test": "./node_modules/mocha/bin/mocha --compilers coffee:coffee-script/register"
  }
}
Use with es6+coffeescript

After done both…

{
  "scripts": {
    "test": "./node_modules/mocha/bin/mocha --compilers js:babel-core/register,coffee:coffee-script/register"
  }
}
# $ mocha
$ npm t
$ npm test

chai

import chai from 'chai';

const assert = chai.assert;
const expect = chai.expect;
const should = chai.should();
foo.should.be.a('string');
foo.should.equal('bar');
list.should.have.length(3);
obj.should.have.property('name');

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(list).to.have.length(3);
expect(obj).to.have.property('flavors');

assert.typeOf(foo, 'string');
assert.equal(foo, 'bar');
assert.lengthOf(list, 3);
assert.property(obj, 'flavors');

Test

测试的一个基本思路是,本身从函数的挪用者动身,对函数举行种种状况的挪用,检察其容错水平、返回效果是不是相符预期。

import chai from 'chai';
const assert = chai.assert;
const expect = chai.expect;
const should = chai.should();

describe('describe a test', () => {

  it('should return true', () => {
      let example = true;
      // expect
      expect(example).not.to.equal(false);
      expect(example).to.equal(true);
      // should
      example.should.equal(true);
      example.should.be.a(boolen);
      [1, 2].should.have.length(2);
  });
  
  it('should check an object', () => {
    // 关于多层嵌套的Object而言..
    let nestedObj = {
        a: {
          b: 1
      }
    };
    let nestedObjCopy = Object.assign({}, nestedObj);
    nestedObj.a.b = 2;
    
    // do a function to change nestedObjCopy.a.b 
    expect(nestedObjCopy).to.deep.equal(nestedObj);
    expect(nestedObjCopy).to.have.property('a');
  });
});

AsynTest

Testing Asynchronous Code with MochaJS and ES7 async/await

mocha没法自动监听异步要领的完成,须要我们在完成以后手动挪用done()要领

而假如要在回调以后运用异步测试语句,则须要运用try/catch举行捕捉。胜利则done(),失利则done(error)

// 一般的测试要领
it("should work", () =>{
  console.log("Synchronous test");
});
// 异步的测试要领
it("should work", (done) =>{
  setTimeout(() => {
    try {
        expect(1).not.to.equal(0);
        done(); // 胜利
    } catch (err) {
        done(err); // 失利
    }
  }, 200);
});

异步测试有两种要领完毕:done或许返回Promise。而经由历程返回Promise,则不再须要编写笨重的try/catch语句

it("Using a Promise that resolves successfully with wrong expectation!", function() {
    var testPromise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve("Hello World!");
        }, 200);
    });

    return testPromise.then(function(result){
        expect(result).to.equal("Hello!");
    });
});

mock

mock是一个接口模仿库,我们能够经由历程它来模仿代码中的一些异步操纵

React单元测试

Test React Component

React组件没法直接经由历程上述要领举行测试,须要装置enzyme依靠。

$ npm i --save-dev enzyme
#
$ npm i --save-dev react-addons-test-utils

假设有如许一个组件:

// ...省略部份import代码
class TestComponent extends React.Component {
  constructor(props) {
    super(props);
    let {num} = props;
    this.state = {
      clickNum: num
    }
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    let {clickNum} = this.state;
    this.setState({
      clickNum: clickNum + 1
    });
  }

  render() {
    let {clickNum} = this.state;
    return (
      <div className="test_component">
        {clickNum}
        <span onClick={this.handleClick}>点我加1</span>
      </div>
    )
  }
}

运用样例:

import React from 'react';
import {expect} from 'chai';
import {shallow} from 'enzyme';

import TestComponent from '../components/TestComponent';

describe('Test TestComponent', () => {
  // 建立一个假造的组件
  const wrapper = shallow(
      <TestComponent num={10} />/
  );

  /* 
  * 以后,我们能够:
  * 经由历程wrapper.state()拿到组件的state
  * 经由历程wrapper.instance()拿到组件实例,以此挪用组件内的要领
  * 经由历程wrapper.find()找到组件内的子组件
  * 然则,没法经由历程wrapper.props()拿到组件的props
  */

  // 测试该组件组外层的class
  it('should render with currect wrapper', () => {
    expect(wrapper.is('.test_component')).to.equal(true);
  });

  // 测试该组件初始化的state
  it('should render with currect state', () => {
    expect(wrapper.state()).to.deep.equal({
      clickNum: 10
    });
  });

  // 测试组件的要领
  it('should add one', () => {
    wrapper.instance().handleClick();
    expect(wrapper.state()).to.deep.equal({
      clickNum: 11
    });
  });
});

Test Redux

redux身为纯函数,异常便于mocha举行测试

// 测试actions
import * as ACTIONS from '../redux/actions';

describe('test actions', () => {
  it('should return an action to create a todo', () => {
    let expectedAction = {
        type: ACTIONS.NEW_TODO,
        todo: 'this is a new todo'
    };
     expect(ACTIONS.addNewTodo('this is a new todo')).to.deep.equal(expectedAction);
  });
});
// 测试reducer
import * as REDUCERS from '../redux/reducers';
import * as ACTIONS from '../redux/actions';

describe('todos', () => {
  let todos = [];
  it('should add a new todo', () => {
      todos.push({
        todo: 'new todo',
        complete: false
    });
    expect(REDUCERS.todos(todos, {
        type: ACTIONS.NEW_TODO,
        todo: 'new todo'
    })).to.deep.equal([
      {
          todo: 'new todo',
          complete: false
      }
    ]);
  });
});
// 还能够和store混用
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import chai from 'chai';
import thunkMiddleware from 'redux-thunk';
import * as REDUCERS from '../redux/reducers';
import defaultState from '../redux/ConstValues';
import * as ACTIONS from '../redux/actions'

const appReducers = combineReducers(REDUCERS);
const AppStore = createStore(appReducers, defaultState, applyMiddleware(thunk));
let state = Object.assign({}, AppStore.getState());

// 一旦注册就会时候监听state变化
const subscribeListener = (result, done) => {
  return AppStore.subscribe(() => {
    expect(AppStore.getState()).to.deep.equal(result);
    done();
  });
};

describe('use store in unittest', () => {
  it('should create a todo', (done) => {
    // 起首获得我们的希冀值
      state.todos.append({
        todo: 'new todo',
        complete: false
    });
    
    // 注册state监听
    let unsubscribe = subscribeListener(state, done);
    AppStore.dispatch(ACTIONS.addNewTodo('new todo'));
    // 完毕以后作废监听
    unsubscribe();
  });
});

基于phantomjsselenium的UI UnitTest

PhantomJS是一个基于webkit的服务器端JavaScript API,即相当于在内存中跑了个无界面的webkit内核的浏览器。经由历程它我们能够模仿页面加载,并获取到页面上的DOM元素,举行一系列的操纵,以此来模仿UI测试。但瑕玷是没法及时瞥见页面上的状况(不过能够截图)。

Selenium是特地为Web应用程序编写的一个验收测试东西,它直接运转在浏览器中。Selenium测试通常会调起一个可见的界面,但也能够经由历程设置,让它以PhantomJS的情势举行无界面的测试。

  • open 某个 url

  • 监听 onload 事宜

  • 事宜完成后挪用 sendEvent 之类的 api 去点击某个 DOM 元素地点 point

  • 触发交互

  • 依据 UI 交互状况 延时 setTimeout (躲避惰加载组件点不到的状况)继承 sendEvent 之类的交互

Getting started with Selenium Webdriver for node.js

    原文作者:ecmadao
    原文地址: https://segmentfault.com/a/1190000006933557
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞