摘要:如果覺得單元測試難以入手不妨嘗試本文方法狀態管理確實帶來十分大的便利但隨之而來的單元測試實現卻令人頭痛至少剛開始我不知道從何著手尤其單元測試更甚本文意旨簡單實現單元測試實現工具測試管理工具測試框架測試斷言庫項目結構前端相關測試相關配置文件入
如果覺得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.lockkarma搭建
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...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86598.html
摘要:項目開發準備描述項目技術選型接口接口文檔測試接口啟動項目開發使用腳手架創建項目開發環境運行生產環境打包運行管理項目創建遠程倉庫創建本地倉庫配置將本地倉庫推送到遠程倉庫在本地創建分支并推送到遠程如果本地有修改新的同事克隆倉庫如果遠程修 day01 1. 項目開發準備 1). 描述項目 2). 技術選型 3). API接口/接口文檔/測試接口 2. 啟動項目開發 1). 使用react...
摘要:對模塊進行了打包,監聽文件更改刷新等功能,創建了個服務,分別為靜態資源服務用于代理本地資源,與自刷新瀏覽器請求服務用于接受,請求,返回數據服務用于收發消息。除了項目,還可以換成項目。項目地址如果覺得對你有所幫助,多謝支持 prince-cli 快速指南 這是一個為快速創建SPA所設計的腳手架,旨在為開發人員提供簡單規范的開發方式、服務端環境、與接近native應用的體驗。使用它你能夠獲...
摘要:而中實現原理是利用高階函數通過將多個函數組合成一個可執行執行函數關鍵步驟代碼如下所示。和都是基于更新差異元素。 引言 平時開發單頁項目應用基于vue,目前另外兩個比較熱的庫還有angular和react,angular 1系列用過,進入公司后由于基于vue技術棧就沒在關注了。一直在關注react,目的不是學習用法,只是為了拓展自己的視野和思維,通過了解一些使用上的差異性,來進一步的思考...
摘要:舉例來說一個異步的請求場景,可以如下實現任何異步的邏輯都可以,如等等也可以使用的和。實際上在中,一個就是一個函數。 書籍完整目錄 3.4 redux 異步 showImg(https://segmentfault.com/img/bVyou8); 在大多數的前端業務場景中,需要和后端產生異步交互,在本節中,將詳細講解 redux 中的異步方案以及一些異步第三方組件,內容有: redu...
摘要:通過創建將所有的異步操作邏輯收集在一個地方集中處理,可以用來代替中間件。 redux-saga框架使用詳解及Demo教程 前面我們講解過redux框架和dva框架的基本使用,因為dva框架中effects模塊設計到了redux-saga中的知識點,可能有的同學們會用dva框架,但是對redux-saga又不是很熟悉,今天我們就來簡單的講解下saga框架的主要API和如何配合redux框...
閱讀 2296·2023-04-25 16:42
閱讀 1202·2021-11-22 14:45
閱讀 2338·2021-10-19 13:10
閱讀 2827·2021-09-29 09:34
閱讀 3408·2021-09-23 11:21
閱讀 2100·2021-08-12 13:25
閱讀 2180·2021-07-30 15:15
閱讀 3491·2019-08-30 15:54