iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 29
1
Modern Web

寫React的那些事系列 第 29

React Day29 - Redux Testing(1)

Unit test是軟體開發不可缺少的一部分,通常一個功能的完成要伴隨相對應的unit test,它可以提升開發時的速度,結合自動化流程讓我們不用一直手動測試相同的地方。今天我想介紹在Redux專案中testing的方式,首先要先來簡單講一下Mocha

Mocha


Mocha是很紅的javascript測試框架,一般來說mocha的架構如下:

describe('Test suite description', () => {
  it('Test case description', () => {
    // assertion
    expect(someFunc()).to.be.equal(1);
  });
});

#Test suite

Test suite就是describe這個區塊,表示一組相關功能的測試項目,第一個參數傳入這組相關功能的描述,第二個參數是一個function,裡面包含一個或多個Test case,甚至也可以包含其他Test suite。

#Test case

Test case就是it這個區塊,表示一個測試項目,第一個參數一樣傳入測試項目的描述,第二個參數也是一個function,裡面驗證某個功能,可以包含多個assertion。

#Assertion

Assertion就是expect這一行,它是斷言的意思,用來判斷執行的結果是否如預期。如果不如預期,這個測試項目就會fail。Mocha本身沒有包含assertion功能,通常我們需要再引用其他的assertion library,例如:chaiexpect.jsshould.js。之後範例會使用chai,以下先簡單介紹。

Chai


它裡面有三種style,should、expect、assert,在官網 Styles上可以直接看到這三種風格寫法,我覺得都滿好懂的,都是看指令就可以知道斷言的內容,非常口語化,可以挑一種你覺得最投緣的寫法XD。

接下來我會使用expect style,expect和should都屬於BDD style,它們初始化方式不同但使用方式類似,都有Language Chains提供一些口語的字來幫助組合斷言,例如:to、be、is、has...等,這些字只是用來增加測試的可讀性,並沒有判斷的功能。以下說明幾個有判斷功能的API。

基本語法

/* 相等,equal是嚴格等於 === */

// 字串與數值的相等
expect('hello').to.equal('hello');
expect(42).to.equal(42);

// 比對值的相等,deep comparison
expect({ foo: 'bar'}).to.deep.equal({foo: 'bar'});

// boolean值的相等
expect(true).to.be.true;


/* 不相等,使用not來否定 === */

// 因為equal是嚴格等於
expect(1).to.not.equal(true);

// 因為不同object不相等,除非加deep
expect({ foo: 'bar'}).to.not.equal({ foo: 'bar'});

// boolean值為否定
expect(false).to.not.be.ok;


/* 其他比較方式 */

// typeof
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');

// include
expect([1,2,3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');

// empty,長度為0,或是
expect([]).to.be.empty;
expect('').to.be.empty;
expect({}).to.be.empty;

// NaN
expect('foo').to.be.NaN;
expect(4).not.to.be.NaN;

// match
expect('foobar').to.match(/^foo/);

// hasOwnProperty
expect('test').to.have.ownProperty('length');

是不是一看就知道要判斷什麼內容呢?Chai提供許多口語化的判斷的方式,可以視情況需要使用,盡量用容易明白的方式來撰寫斷言,官網有更詳細的API介紹

撰寫測試之前


如同前面提到,我們必須先安裝mocha、chai這兩個packages:

npm install mocha chai --save-dev

我們先設定mocha的指令在 package.json 的script中:

  • Mocha預設會執行 test 資料夾下第一層的檔案,所以這邊不用特別設定test檔案路徑。
  • 因為mocha預設只有 test 第一層,當我們有第二層以上的資料夾,需加上--recursive讓mocha每一層資料夾內的檔案都執行。
  • 使用babel-core/register來編譯test檔案,讓ES6語法可以編譯後再執行。
"scripts": {
  // ...
  "test": "mocha --compilers js:babel-core/register --recursive"
},

然後,設定 .babelrc 讓babel-core/register知道要編譯ES6:

{
  "presets": ["es2015"]
}

如果mocha的指令太長,也可以在test目錄下建立 mocha.opts ,來管理mocha相關的指令,例如:

--compilers js:babel-core/register
--recursive

在Redux中我們針對action、reducer、component都有相對應的測試,測試的檔名通常會用原檔名,後面可以加.test.js或者.spec.js.test.js結尾通常表示測試,.spec.js結尾通常表示規格,在我們的test中可以看個人習慣決定要使用哪一個結尾,後面範例我會使用.test.js

撰寫Action tests


在redux中action是plain object,透過action creator回傳action,所以我們針對action creator來寫test,判斷它回傳的action是否正確。以下使用我們filter action來做範例。

test/actions/filter.test.js

import { expect } from 'chai';
import * as types from '../../src/constants/ActionTypes';
import * as filter from '../../src/actions/filter';

describe('filter action testing', () => {
  it('should create an action to set filter', () => {
    expect(
      filter.setFilter('SHOW_ALL')
    ).to.be.deep.equal(
      {
        type: types.SET_FILTER,
        filter: 'SHOW_ALL'
      }
    );
  });
});

透過前面介紹的mocha寫法與chai的斷言,這樣的test很容易可以了解。不過,如果是非同步的action creator,我們還需要加上幾個步驟來撰寫測試。

撰寫Async action tests


因為我們是透過redux-thunk這個middleware來達成非同步的功能,所以在test時,要能夠mock store把middleware加進去,這邊我們透過redux-mock-store,來幫助我們mock store。

先安裝package:

npm install redux-mock-store --save-dev

這邊使用todos action的addTask為範例:

import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { expect } from 'chai';
import * as types from '../../src/constants/ActionTypes';
import * as todos from '../../src/actions/todos';

// 建立mock store
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe('todos action testing', () => {
  it('should create an action to add task', () => {
    const task = 'Learn mocha';
    const expectedActions = [
      { type: types.ADD_TASK_REQUEST },
      { type: types.ADD_TASK_SUCCESS, task }
    ];

    // mockStore裡面傳入會使用到的state,但這邊沒有所以放空的
    const store = mockStore({});
    // 模擬dispatch action
    store.dispatch(todos.addTask(task));
    // 原先範例是過一秒執行,這邊一秒後store.getActions()
    setTimeout(() => {
      expect(
        store.getActions()
      ).to.be.deep.equal(expectedActions);
    }, 1000);
  });
});

透過mock store,我們把redux-thunk middleware加入,就可以模擬非同步的action。在實際上,通常會用到非同步是因為到後端fetch,我們可以透過nock來moch HTTP,以下是範例說明:

先安裝package:

npm install nock --save-dev

假設我們的action如下:

export function addTaskRequest(){
  return {
    type: 'ADD_TASK_REQUEST'
  };
}

export function addTaskSuccess(task){
  return {
    type: 'ADD_TASK_SUCCESS',
    task
  };
}

export function addTaskFailure(err){
  return {
    type: 'ADD_TASK_FAILURE',
    err
  };
}

export function addTask(task){
  return (dispatch) => {
    dispatch(addTaskRequest());
    return fetch('http://www.domain.com/saveData', {
      method: 'POST',
      body: JSON.stringify({
        task
      })
    })
    .then(response => {
      dispatch(addTaskSuccess(task));
    })
    .catch(err => dispatch(addTaskFailure(err)));
  };
}

action.test.js測試addTask可以這樣寫:

import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import nock from 'nock';  // 記得安裝nock package
import { expect } from 'chai';
import * as types from '../../src/constants/ActionTypes';
import * as todos from '../../src/actions/todos';

// 建立mock store
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe('todos action testing', () => {
  afterEach(() => {
    // 每個test case執行完,都做nock clean
    nock.cleanAll();
  });

  it('should create an action to add task', () => {
    const task = 'Learn mocha';
    const expectedActions = [
      { type: types.ADD_TASK_REQUEST },
      { type: types.ADD_TASK_SUCCESS, task }
    ];

    // mock HTTP連線
    nock('http://www.domain.com')
      .post('/saveData', JSON.stringify({ task }))
      .reply(200);

    const store = mockStore({});
    return store.dispatch(todos.addTask(task))
      .then(() => {
        expect(
          store.getActions()
        ).to.be.deep.equal(expectedActions);
      });
  });
});

上面是模擬post的情境,nock官網上有更多其他使用方式,也可以上去看更多喔!

今天我們已經介紹如何撰寫action test,我有把其他actions的unit test都補完,目前的code已經放到Git 上囉!

參考


Redux Testing
測試框架 Mocha 實例教程


上一篇
React Day28 - ESLint(2)
下一篇
React Day30 - Redux Testing(2)
系列文
寫React的那些事31

尚未有邦友留言

立即登入留言