若是以爲redux async action單元測試難以入手,不妨嘗試本文方法.javascript
redux
狀態管理確實帶來十分大的便利,但隨之而來的單元測試
實現卻使人頭痛(至少剛開始我不知道從何着手).尤爲async action單元測試更甚,本文意旨簡單實現redux async action單元測試.html
karma 測試管理工具前端
mocha 測試框架java
chai 測試斷言庫node
. ├── LICENSE ├── README.md ├── app #前端相關 │ ├── actions #redux actions │ │ └── about.js │ └── helpers #validator │ └── validator.js ├── package.json ├── test #測試相關 │ ├── actions #test redux actions │ │ └── about_test.js │ ├── karma.conf.js #karma配置文件 │ └── test_index.js #test 入口文件 ├── webpack.test.js #test wepack └── yarn.lock
karma配置文件
/** * test/karma.conf.js */ var webpackConfig = require('../webpack.test'); module.exports = function (config) { config.set({ // 使用的測試框架&斷言庫 frameworks: ['mocha', 'chai'], // 測試文件同時做爲webpack入口文件 files: [ 'test_index.js' ], // webpack&sourcemap處理測試文件 preprocessors: { 'test_index.js': ['webpack', 'sourcemap'] }, // 測試瀏覽器 browsers: ['PhantomJS'], // 測試結束關閉PhantomJS phantomjsLauncher: { exitOnResourceError: true }, // 生成測試報告 reporters: ['mocha', 'coverage'], // 覆蓋率配置 coverageReporter: { dir: 'coverage', reporters: [{ type: 'json', subdir: '.', file: 'coverage.json', }, { type: 'lcov', subdir: '.' }, { type: 'text-summary' }] }, // webpack配置 webpack: webpackConfig, webpackMiddleware: { stats: 'errors-only' }, // 自動監測測試文件內容 autoWatch: false, // 只運行一次 singleRun: true, // 運行端口 port: 9876, // 輸出彩色 colors: true, // 輸出等級 // config.LOG_DISABLE // config.LOG_ERROR // config.LOG_WARN // config.LOG_INFO // config.LOG_DEBUG logLevel: config.LOG_INFO }); };
karma測試入口文件
/** * test/test_index.js * 引入test目錄下帶_test文件 */ var testsContext = require.context(".", true, /_test$/); testsContext.keys().forEach(function (path) { try { testsContext(path); } catch (err) { console.error('[ERROR] WITH SPEC FILE: ', path); console.error(err); } });
es6將會已經成爲主流,因此搭建karma
時選擇webpack
配合babel
進行打包處理.react
webpack
/** * webpack.test.js */ process.env.NODE_ENV = 'test'; var webpack = require('webpack'); var path = require('path'); module.exports = { name: 'run test webpack', devtool: 'inline-source-map', //Source Maps module: { loaders: [ { test: /\.jsx|.js$/, include: [ path.resolve('app/'), path.resolve('test/') ], loader: 'babel' } ], preLoaders: [{ //在webpackK打包前用isparta-instrumenter記錄編譯前文件,精準覆蓋率 test: /\.jsx|.js$/, include: [path.resolve('app/')], loader: 'isparta' }], plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('test') }) ] } };
babel
/** * .babelrc */ { "presets": ["es2015", "stage-0", "react"] }
爲了對actions執行了什麼有個具體的概念,此處貼一張圖
webpack
/** * app/actions/about.js */ import 'isomorphic-fetch'; import * as Validators from '../helpers/validator'; export const GET_ABOUT_REQUEST = 'GET_ABOUT_REQUEST'; export const GET_ABOUT_SUCCEED = 'GET_ABOUT_SUCCEED'; export const GET_ABOUT_FAILED = 'GET_ABOUT_FAILED'; export const CHANGE_START = 'CHANGE_START'; export const CHANGE_ABOUT = 'CHANGE_ABOUT'; const fetchStateUrl = '/api/about'; /** * 異步獲取about * method get */ exports.fetchAbout = ()=> { return async(dispatch)=> { // 初始化about dispatch(aboutRequest()); try {//成功則執行aboutSucceed let response = await fetch(fetchStateUrl); let data = await response.json(); return dispatch(aboutSucceed(data)); } catch (e) {//失敗則執行aboutFailed return dispatch(aboutFailed()); } } }; /** * 改變start * value 星數 */ exports.changeStart = (value)=> ({ type: CHANGE_START, value: value, error: Validators.changeStart(value) }); /** * 異步改變about * method post */ exports.changeAbout = ()=> { return async(dispatch)=> { try { let response = await fetch('/api/about', { method: 'POST' }); let data = await response.json(); return dispatch({ type: CHANGE_ABOUT, data: data }); } catch (e) { } } }; const aboutRequest = ()=> ({ type: GET_ABOUT_REQUEST }); const aboutSucceed = (data)=>({ type: GET_ABOUT_SUCCEED, data: data }); const aboutFailed = ()=> { return { type: GET_ABOUT_FAILED } };
由於對星數有限制,編寫validator限制git
validator
/** * app/helpers/validator.js */ // 限制星數必須爲正整數且在1~5之間 export function changeStart(value) { var reg = new RegExp(/^[1-5]$/); if (typeof(value) === 'number' && reg.test(value)) { return '' } return '星數必須爲正整數且在1~5之間' }
這裏測試了actions應該暴露的const
,普通的actions
,異步的actions
.es6
測試async actions
主要靠fetch-mock攔截actions自己,而且返回指望的結果.github
注意:fetch-mock mock(matcher, response, options)方法,matcher使用begin:
匹配相應url.如:begin:http://www.example.com/,即匹配http://www.example.com/也匹配http://www.example.com/api/about
/** * test/actions/about_test.js */ import 'babel-polyfill'; // 轉換es6新的API 這裏主要爲Promise import 'isomorphic-fetch'; // fetchMock依賴 import fetchMock from 'fetch-mock';// fetch攔截並模擬數據 import configureMockStore from 'redux-mock-store';// 模擬store import thunk from 'redux-thunk'; import * as Actions from '../../app/actions/about'; //store經過middleware進行模擬 const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); describe('actions/about', () => { //export constant test describe('export constant', ()=> { it('Should export a constant GET_ABOUT_REQUEST.', () => { expect(Actions.GET_ABOUT_REQUEST).to.equal('GET_ABOUT_REQUEST'); }); it('Should export a constant GET_ABOUT_SUCCEED.', () => { expect(Actions.GET_ABOUT_SUCCEED).to.equal('GET_ABOUT_SUCCEED'); }); it('Should export a constant GET_ABOUT_FAILED.', () => { expect(Actions.GET_ABOUT_FAILED).to.equal('GET_ABOUT_FAILED'); }); it('Should export a constant CHANGE_START.', () => { expect(Actions.CHANGE_START).to.equal('CHANGE_START'); }); it('Should export a constant GET_ABOUT_REQUEST.', () => { expect(Actions.CHANGE_ABOUT).to.equal('CHANGE_ABOUT'); }); }); //normal action test describe('action fetchAbout', ()=> { it('fetchAbout should be exported as a function.', () => { expect(Actions.fetchAbout).to.be.a('function') }); it('fetchAbout should return a function (is a thunk).', () => { expect(Actions.fetchAbout()).to.be.a('function') }); }); describe('action changeStart', ()=> { it('changeStart should be exported as a function.', () => { expect(Actions.changeStart).to.be.a('function') }); it('Should be return an action and return correct results', () => { const action = Actions.changeStart(5); expect(action).to.have.property('type', Actions.CHANGE_START); expect(action).to.have.property('value', 5); }); it('Should be return an action with error while input empty value.', () => { const action = Actions.changeStart(); expect(action).to.have.property('error').to.not.be.empty }); }); describe('action changeAbout', ()=> { it('changeAbout be exported as a function.', () => { expect(Actions.changeAbout).to.be.a('function') }); }); //async action test describe('async action', ()=> { //對每一個執行完的測試恢復fetchMock afterEach(fetchMock.restore); describe('action fetchAbout', ()=> { it('Should be done when fetch action fetchAbout', async()=> { const data = { "code": 200, "msg": "ok", "result": { "value": 4, "about": "it's my about" } }; // 指望的發起請求的 action const actRequest = { type: Actions.GET_ABOUT_REQUEST }; // 指望的請求成功的 action const actSuccess = { type: Actions.GET_ABOUT_SUCCEED, data: data }; const expectedActions = [ actRequest, actSuccess, ]; //攔截/api/about請求並返回自定義數據 fetchMock.mock(`begin:/api/about`, data); const store = mockStore({}); await store.dispatch(Actions.fetchAbout()); //比較store.getActions()與指望值 expect(store.getActions()).to.deep.equal(expectedActions); }); it('Should be failed when fetch action fetchAbout', async()=> { // 指望的發起請求的 action const actRequest = { type: Actions.GET_ABOUT_REQUEST }; // 指望的請求失敗的 action const actFailed = { type: Actions.GET_ABOUT_FAILED }; const expectedActions = [ actRequest, actFailed, ]; //攔截/api/about請求並返回500錯誤 fetchMock.mock(`begin:/api/about`, 500); const store = mockStore({}); await store.dispatch(Actions.fetchAbout()); //比較store.getActions()與指望值 expect(store.getActions()).to.deep.equal(expectedActions); }); }); describe('action changeAbout', ()=> { it('Should be done when fetch action changeAbout', async()=> { const data = { "code": 200, "msg": "ok", "result": { "about": "it's changeAbout fetch about" } }; const acSuccess = { type: Actions.CHANGE_ABOUT, data: data }; const expectedActions = [ acSuccess ]; //攔截/api/about post請求並返回自定義數據 fetchMock.mock(`begin:/api/about`, data, {method: 'POST'}); const store = mockStore({}); await store.dispatch(Actions.changeAbout()); //比較store.getActions()與指望值 expect(store.getActions()).to.deep.equal(expectedActions); }); }); }); });
"dependencies": { "isomorphic-fetch": "^2.2.1", "react": "^15.4.1", "react-dom": "^15.4.1", "redux": "^3.6.0", "webpack": "^1.14.0" }, "devDependencies": { "babel-cli": "^6.18.0", "babel-loader": "^6.2.10", "babel-polyfill": "^6.20.0", "babel-preset-es2015": "^6.18.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-0": "^6.16.0", "chai": "^3.5.0", "fetch-mock": "^5.8.0", "isparta-loader": "^2.0.0", "karma": "^1.3.0", "karma-chai": "^0.1.0", "karma-coverage": "^1.1.1", "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.1", "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.8.1", "mocha": "^3.2.0", "redux-mock-store": "^1.2.1", "redux-thunk": "^2.1.0", "sinon": "next" }
直接在項目根目錄中執行npm test
則能夠進行測試
"scripts": { "test": "./node_modules/karma/bin/karma start test/karma.conf.js" }
測試結果
與Github
進行綁定
每次push執行npm test
進行測試
因爲Travis默認測試Ruby項目,因此在根目錄下添加.travis.yml
文件
language: node_js #項目標註爲javascript(nodeJs) node_js: '6' #nodeJs版本 sudo: true cache: yarn #yarn緩存目錄 $HOME/.yarn-cache
https://github.com/timmyLan/r...