资讯专栏INFORMATION COLUMN

redux async action 轻松搭建测试环境

KevinYan / 2727人阅读

摘要:如果觉得单元测试难以入手不妨尝试本文方法状态管理确实带来十分大的便利但随之而来的单元测试实现却令人头痛至少刚开始我不知道从何着手尤其单元测试更甚本文意旨简单实现单元测试实现工具测试管理工具测试框架测试断言库项目结构前端相关测试相关配置文件入

如果觉得redux async action单元测试难以入手,不妨尝试本文方法.

redux状态管理确实带来十分大的便利,但随之而来的单元测试实现却令人头痛(至少刚开始我不知道从何着手).尤其async action单元测试更甚,本文意旨简单实现redux async action单元测试.

实现工具

karma 测试管理工具

mocha 测试框架

chai 测试断言库

项目结构
.
├── 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进行打包处理.

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执行了什么有个具体的概念,此处贴一张图

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

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.

测试async actions主要靠fetch-mock拦截actions本身,并且返回期望的结果.

注意: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"
  }

测试结果

持续集成

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

若项目通过可得到属于该项目的小图标

项目地址

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

参考资料

http://cn.redux.js.org/docs/r...

https://github.com/wheresrhys...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86598.html

相关文章

  • 尚学堂 react -后台管理系统开发流程

    摘要:项目开发准备描述项目技术选型接口接口文档测试接口启动项目开发使用脚手架创建项目开发环境运行生产环境打包运行管理项目创建远程仓库创建本地仓库配置将本地仓库推送到远程仓库在本地创建分支并推送到远程如果本地有修改新的同事克隆仓库如果远程修 day01 1. 项目开发准备 1). 描述项目 2). 技术选型 3). API接口/接口文档/测试接口 2. 启动项目开发 1). 使用react...

    lemon 评论0 收藏0
  • 使用prince-cli,轻松构建高性能React SPA项目~

    摘要:对模块进行了打包,监听文件更改刷新等功能,创建了个服务,分别为静态资源服务用于代理本地资源,与自刷新浏览器请求服务用于接受,请求,返回数据服务用于收发消息。除了项目,还可以换成项目。项目地址如果觉得对你有所帮助,多谢支持 prince-cli 快速指南 这是一个为快速创建SPA所设计的脚手架,旨在为开发人员提供简单规范的开发方式、服务端环境、与接近native应用的体验。使用它你能够获...

    roundstones 评论0 收藏0
  • vue和react的差异

    摘要:而中实现原理是利用高阶函数通过将多个函数组合成一个可执行执行函数关键步骤代码如下所示。和都是基于更新差异元素。 引言 平时开发单页项目应用基于vue,目前另外两个比较热的库还有angular和react,angular 1系列用过,进入公司后由于基于vue技术栈就没在关注了。一直在关注react,目的不是学习用法,只是为了拓展自己的视野和思维,通过了解一些使用上的差异性,来进一步的思考...

    OnlyLing 评论0 收藏0
  • 精益 React 学习指南 (Lean React)- 3.4 掌控 redux 异步

    摘要:举例来说一个异步的请求场景,可以如下实现任何异步的逻辑都可以,如等等也可以使用的和。实际上在中,一个就是一个函数。 书籍完整目录 3.4 redux 异步 showImg(https://segmentfault.com/img/bVyou8); 在大多数的前端业务场景中,需要和后端产生异步交互,在本节中,将详细讲解 redux 中的异步方案以及一些异步第三方组件,内容有: redu...

    JouyPub 评论0 收藏0
  • redux-saga框架使用详解及Demo教程

    摘要:通过创建将所有的异步操作逻辑收集在一个地方集中处理,可以用来代替中间件。 redux-saga框架使用详解及Demo教程 前面我们讲解过redux框架和dva框架的基本使用,因为dva框架中effects模块设计到了redux-saga中的知识点,可能有的同学们会用dva框架,但是对redux-saga又不是很熟悉,今天我们就来简单的讲解下saga框架的主要API和如何配合redux框...

    Nosee 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<