redux async action 輕鬆搭建測試環境

若是以爲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搭建

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

爲了對actions執行了什麼有個具體的概念,此處貼一張圖
about.pngwebpack

/**
 * 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

"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"
  }

script

直接在項目根目錄中執行npm test則能夠進行測試

"scripts": {
    "test": "./node_modules/karma/bin/karma start test/karma.conf.js"
  }

測試結果
testInfo.png

持續集成

  • Travis-cli

    • 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

若項目經過可獲得屬於該項目的小圖標 Build Status

項目地址

https://github.com/timmyLan/r...

參考資料

相關文章
相關標籤/搜索