使用Mocha + Chai + Sinon 測試React + Redux的web應用

今天來介紹一下如何使用Mocha + Chai + Sinon測試基於Redux + React的web應用,以及介紹一些在使用過程當中遇到的問題和解決方法。 css

Mocha

Mocha是一個JS的測試框架,相似於Java中的Junit、Python中的nose。Mocha的使用很是簡單,使用describe和it就能夠寫單元測試,下面是代碼示例。 html

1 2 3 4 5 6 7 8 9 10
import {expect} from 'chai';  describe('Array', function() {  describe('#indexOf()', function () {  it('should return -1 when the value is not present', function () {  expect([1,2,3].indexOf(5)).to.be.equal(-1);  expect([1,2,3].indexOf(0)).to.be.equal(-1);  });  }); }); 

Chai

Chai是一個單元測試的驗證框架,它有3種不一樣形式的校驗:expect、should和assert。expect和should的方式讓寫出來的測試代碼更像天然語言,讓業務人員也能夠看懂,而assert方式是傳統單元測試斷言的方式,若是之前習慣寫Java的單元測試會對這種方式比較熟悉。 前端

Sinon

Sinon是一個mock框架,相似Java的mockito。它能夠對任何對象進行mock,更重要的是它提供了一些對mock對象的校驗方法。 react

1 2 3 4 5 6 7 8 9 10 11 12 13 14
// 建立mock對象 const callback = sinon.spy(); // 調用測試方法 const proxy = once(callback);  // 校驗mock對象是否被調用; assert(callback.called); // 校驗被調用了多少次 assert(callback.calledOnce); assert.equals(callback.callCount, 1); // 校驗被哪一個對象調用  assert(callback.calledOn(obj)); // 校驗被調用時傳入了哪些參數 assert(callback.calledWith(1, 2, 3)); 

Redux + React

React不用介紹了,今年最火的一個前端框架,而Redux是一個傳遞、處理state數據的JS框架,配合React能夠很方便的處理staet數據,從而達到經過state控制渲染頁面的目的。做者Dan Abramov本身拍了一個Redux的教學視頻,裏面經過一個個demo演示瞭如何寫react和redux,視頻能夠見這裏webpack

對於Redux和React的應用,最主要的代碼有3個部分,分別是actions,reducers,components。actions是發送一個狀態到reducers,reducers根據狀態返回修改後的state,components接收到state後刷新頁面,因此咱們的測試主要針對這3個部分。 git

actons測試

action的代碼多是這樣的,接收從component傳過來的一個參數,返回一個帶有type屬性的一個對象。 github

1 2 3
export function addTodo(text) {  return {type: ADD_TODO, text}; } 

actions的測試比較簡單,就是返回一個對象,測試代碼能夠這樣寫: web

1 2 3 4 5 6 7 8 9 10 11
import {expect} from 'chai'; import * as actions from 'actions/todos';  describe('todo actions', () => {  it('add todo should create ADD_TODO action', () => {  expect(actions.addTodo('Use Redux')).to.deep.equal({  type: 'add_todo',  text: 'Use Redux',  });  }); }); 

這裏使用了chai的expect校驗方式,傳入一個字符串,驗證是否返回正確的對象,這裏使用了to.deep.equal這個校驗方法,能夠校驗對象的屬性是否相等,而對於number、bool等基本類型的校驗可使用to.be.equal這個校驗方法。 npm

reducers測試

reducers代碼以下,在原來的state基礎上加上一個新的todo對象。 redux

1 2 3 4 5 6 7 8 9 10 11 12 13
export default function todos(state = initState, action) {  switch (action.type) {  case ADD_TODO:  return [  ...state,  {  text: action.text,  completed: false,  id: new Date().getTime(),  },  ];  } } 

測試代碼能夠這樣寫:

1 2 3 4 5 6 7 8 9 10
describe('reducers', () => {  describe('todos', () => {  it('should add todo correctly', () => {  const state = todos({}, {type: ADD_TODO, text: 'foo'});  expect(state.length).to.be.equal(1);  expect(state[0].text).to.be.equal('foo');  expect(state[0].completed).to.be.equal(false);  });  }); }); 

測試時傳入一個空的state對象和一個action對象,驗證返回的state是否增長了一個todo對象。

components測試

components的測試比較複雜,除了測試render後的頁面,還須要測試一些component的DOM方法,好比click,change,doubleclick等。下面是一個Header組件,它有h1和另一個自定義組件TodoInput,其中還有一個handleSave的自定義方法,因此咱們要測試的就主要是render和這個方法。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
import React, { PropTypes, Component } from 'react'; import TodoInput from './TodoInput'; class Header extends Component {  handleSave(text) {  if (text && text.length !== 0) {  this.props.actions.addTodo(text);  }  }  render() {  return (  <header className="header">  <h1>Todo List</h1>  <TodoInput newTodo placeholder="請錄入..." onSave={(text) => this.handleSave(text)}/>  </header>  );  } } Header.propTypes = {  actions: PropTypes.object.isRequired, }; export default Header; 

測試React的component,須要用到react的一個測試組件Test Utils,在寫測試代碼以前,須要先構造組件render的頁面,渲染頁面的props參數和render頁面的對象,這些在後面的測試中很是有用。

1 2 3 4 5 6 7 8 9 10 11 12 13 14
import sinon from 'sinon'; import {expect} from 'chai'; import React from 'react'; import TestUtils from 'react-addons-test-utils'; import Header from 'components/Header'; import TodoInput from 'components/TodoInput'; function setup() {  const actions = {addTodo: sinon.spy()};  const props = {actions: actions};  const renderer = TestUtils.createRenderer();  renderer.render(<Header {...props} />);  const output = renderer.getRenderOutput();  return {props, output, renderer}; } 

構造完這些對象後,咱們先對render方法進行測試。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
describe('Header', () => {  it('should render correctly', () => {  const { output } = setup();   expect(output.type).to.be.equal('header');  expect(output.props.className).to.be.equal('header');   const [ h1, input ] = output.props.children;   expect(h1.type).to.be.equal('h1');  expect(h1.props.children).to.be.equal('Todo List');   expect(input.type).to.be.equal(TodoInput);  expect(input.props.newTodo).to.be.equal(true);  expect(input.props.placeholder).to.be.equal('請錄入...');  }); }); 

首先測試component的第一層——header,驗證其type和className,而後經過children獲取其下層組件h1和TodoInput,再對這2個組件進行校驗。

接着測試TodoInput的onSave方法,它實際調用的是handleSave方法,方法會判斷參數text的長度是否爲0來決定是否調用actions的addTodo方法。

1 2 3 4 5 6 7 8
it('should call addTodo if length of text is greater than 0', () => {  const { output, props } = setup();  const input = output.props.children[1];  input.props.onSave('');  expect(props.actions.addTodo.callCount).to.be.equal(0);  input.props.onSave('Use Redux');  expect(props.actions.addTodo.callCount).to.be.equal(1); }); 

這裏使用sinon把action的addTodo方法mock掉了,而後再驗證該方法是否有調用。

React組件使用了CSS文件

在寫React的components時可能會加上本身定義的一些css文件(或者是less和sass等),這在mocha運行測試時會報錯,報沒法解析css語法的錯誤。咱們能夠經過編寫自定義的mocha css編譯器來解決這個問題。

css-null-compiler.js
1 2 3 4 5 6 7 8 9
function noop() {  return null; }  require.extensions['.styl'] = noop; // you can add whatever you wanna handle require.extensions['.scss'] = noop; require.extensions['.css'] = noop; // ..etc 

而後在運行mocha時加上剛寫的編譯器:mocha /your/test.spec.js --compilers css:css-null-compiler.js。

webpack使用了alias

在使用webpack時咱們會經過別名(alias)的方法來簡化咱們import其餘文件時的路徑,好比原來import時須要這樣寫:

css-null-compiler.js
1
import Header from '../../src/components/Header';

使用了alias以後能夠這樣:

css-null-compiler.js
1
import Header from 'src/components/Header';

可是這種路徑在測試的時候就會報找不到文件路徑的錯誤,由於直接使用Mocha運行測試時並無設置路徑別名。

所以咱們須要使用幾個工具來解決這個問題,分別是mock-requireproxyquire

首先在mocha的before方法中經過mock-require來替換別名路徑,而後在mocha的beforeEach中用proxyquire來調用被測試的module,具體代碼以下:

css-null-compiler.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import proxyquire from 'proxyquire'; import mockrequire from 'mock-require';  before(() => {  // mock the alias path, point to the actual path  mockrequire('actions/youractions', 'your/actual/action/path/from/your/test/file');  // or mock with a function  mockrequire('actions/youractions', {actionMethod: () => {...}));  let app; beforeEach(() => {  app = proxyquire('./app', {}); });  //test code describe('xxx', () => {  it('xxxx', () => {  ...  }); }); 

React的組件中使用了DOM變量

在作components測試時還會遇到一個問題,好比在某些組件中使用了DOM的一些全局變量,好比window,document等,這些只有在瀏覽器中才會有,而mocha測試咱們是在命令行中執行的,並無瀏覽器的這些變量。

要解決這個問題有2種方法,一種是使用Karma來作單元測試。Karma是一個測試運行器,它會啓動一個瀏覽器來運行測試,比較適合端到端的頁面測試。但單元測試要使用瀏覽器來運行就顯得有點浪費了,並且會影響測試的速度。

因此咱們使用第二種方法,使用jsdom來模擬DOM結構,首先咱們要建立一個js文件來模擬DOM。

dom.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
import jsdom from 'jsdom'; import mockrequire from 'mock-require'; // setup the simplest document possible const doc = jsdom.jsdom('<!doctype html><html><body></body></html>'); // get the window object out of the document const win = doc.defaultView; // set globals for mocha that make access to document and window feel // natural in the test environment global.document = doc; global.window = win; // from mocha-jsdom https://github.com/rstacruz/mocha-jsdom/blob/master/index.js#L80 function propagateToGlobal(window) {  for (const key in window) {  if (!window.hasOwnProperty(key)) continue;  if (key in global) continue;  global[key] = window[key];  }  window.matchMedia = window.matchMedia || function matchMedia() {  return {  matches: false,  addListener: () => {},  removeListener: () => {},  };  }; }  propagateToGlobal(); 

而後在mocha.opts文件中加入對這個文件的引用,mocha.opts文件是mocha的配置文件,通常放在test目錄下面,經過配置該文件能夠在調用mocha命令時少寫一些參數。

dom.js
1 2 3
--require test/dom.js --reporter dot --ui bdd 

這樣之後在運行mocha時就會自動加載dom.js文件了。

相關文章
相關標籤/搜索