摘要:可以監(jiān)控文件變化自動(dòng)執(zhí)行單元測(cè)試,可以緩存測(cè)試結(jié)果,可以顯示測(cè)試過(guò)程中的變量測(cè)試框架。執(zhí)行單元測(cè)試三測(cè)試在的理念中,組件應(yīng)該分為視覺(jué)組件和高階組件,與邏輯分離,更利于測(cè)試。
一、工具介紹
karma:測(cè)試過(guò)程管理工具??梢员O(jiān)控文件變化自動(dòng)執(zhí)行單元測(cè)試,可以緩存測(cè)試結(jié)果,可以console.log顯示測(cè)試過(guò)程中的變量
mocha:測(cè)試框架。提供describe,it,beforeEach等函數(shù)管理你的 testcase,后面示例中會(huì)看到
chai:BDD(行為驅(qū)動(dòng)開(kāi)發(fā))和TDD(測(cè)試驅(qū)動(dòng)開(kāi)發(fā))雙測(cè)試風(fēng)格的斷言庫(kù)
enzyme:React測(cè)試工具,可以類(lèi)似 jquery 風(fēng)格的 api 操作react 節(jié)點(diǎn)
sinon: 提供 fake 數(shù)據(jù), 替換函數(shù)調(diào)用等功能
二、環(huán)境準(zhǔn)備工具安裝就是 npm install,這里就不再詳述,主要的配置項(xiàng)目在karma.conf.js中,可以參考這個(gè)模板項(xiàng)目 react-redux-starter-kit 。如果項(xiàng)目中用到全局變量,比如jquery, momentjs等,需要在測(cè)試環(huán)境中全局引入,否則報(bào)錯(cuò),例如,在karma.conf中引入全局變量jQuery:
{ files: [ "./node_modules/jquery/jquery.min.js", { pattern: `./tests/test-bundler.js`, watched: false, served: true, included: true } ] }
在test-bundler.js中設(shè)置全局的變量,包括chai, sinon等:
/* tests/test-bundler.js */ import "babel-polyfill" import sinon from "sinon" import chai from "chai" import sinonChai from "sinon-chai" import chaiAsPromised from "chai-as-promised" import chaiEnzyme from "chai-enzyme" chai.use(sinonChai) chai.use(chaiAsPromised) chai.use(chaiEnzyme()) global.chai = chai global.sinon = sinon global.expect = chai.expect global.should = chai.should() ...三、簡(jiǎn)單的函數(shù)測(cè)試
先熱身看看簡(jiǎn)單的函數(shù)如何單元測(cè)試:
/* helpers/validator.js */ export function checkUsername (name) { if (name.length === 0 || name.length > 15) { return "用戶(hù)名必須為1-15個(gè)字" } return "" }
/* tests/helpers/validator.spec.js */ import * as Validators from "helpers/validator" describe("helpers/validator", () => { describe("Function: checkUsername", () => { it("Should not return error while input foobar.", () => { expect(Validators.checkUsername("foobar")).to.be.empty }) it("Should return error while empty.", () => { expect(Validators.checkUsername("")).to.equal("用戶(hù)名必須為1-15個(gè)字") }) it("Should return error while more then 15 words.", () => { expect(Validators.checkUsername("abcdefghijklmnop")).to.equal("用戶(hù)名必須為1-15個(gè)字") expect(Validators.checkUsername("一二三四五六七八九十一二三四五六")).to.equal("用戶(hù)名必須為1-15個(gè)字") }) }) })
describe可以多次嵌套使用,更清晰的描述測(cè)試功能的結(jié)構(gòu)。執(zhí)行單元測(cè)試:
babel-node ./node_modules/karma/bin/karma start build/karma.conf
在 redux 的理念中,react 組件應(yīng)該分為視覺(jué)組件 component 和 高階組件 container,UI與邏輯分離,更利于測(cè)試。redux 的 example 里,這兩種組件一般都分開(kāi)文件去存放。本人認(rèn)為,如果視覺(jué)組件需要多次復(fù)用,應(yīng)該與container分開(kāi)來(lái)寫(xiě),但如果基本不復(fù)用,或者可以復(fù)用的組件已經(jīng)專(zhuān)門(mén)組件化了(下面例子就是),那就沒(méi)必要分開(kāi)寫(xiě),可以寫(xiě)在一個(gè)文件里更方便管理,然后通過(guò) export 和 export default 分別輸出
/* componets/Register.js */ import React, { Component, PropTypes } from "react" import { connect } from "react-redux" import { FormGroup, FormControl, FormLabel, FormError, FormTip, Button, TextInput } from "componentPath/basic/form" export class Register extends Component { render () { const { register, onChangeUsername, onSubmit } = this.props} } Register.propTypes = { register: PropTypes.object.isRequired, onChangeUsername: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired } const mapStateToProps = (state) => { return { register: state.register } } const mapDispatchToProps = (dispatch) => { return { onChangeUsername: name => { ... }, onSubmit: () => { ... } } } export default connect(mapStateToProps, mapDispatchToProps)(Register)用戶(hù)名 請(qǐng)輸入用戶(hù)名 {register.usernameError}
測(cè)試 componet,這里用到 enzyme 和 sinon:
import React from "react" import { bindActionCreators } from "redux" import { Register } from "components/Register" import { shallow } from "enzyme" import { FormGroup, FormControl, FormLabel, FormError, FormTip, Dropdown, Button, TextInput } from "componentPath/basic/form" describe("rdappmsg/trade_edit/componets/Plan", () => { let _props, _spies, _wrapper let register = { username: "", usernameError: "" } beforeEach(() => { _spies = {} _props = { register, ...bindActionCreators({ onChangeUsername: (_spies.onChangeUsername = sinon.spy()), onSubmit: (_spies.onSubmit = sinon.spy()) }, _spies.dispatch = sinon.spy()) } _wrapper = shallow() }) it("Should render as a .", () => { expect(_wrapper.is("div")).to.equal(true) }) it("Should has two children.", () => { expect(_wrapper.children()).to.have.length(2); }) it("Each element of form should be.", () => { _wrapper.children().forEach(function (node) { expect(node.is(FormGroup)).to.equal(true); }) }) it("Should render username properly.", () => { expect(_wrapper.find(TextInput).prop("value")).to.be.empty _wrapper.setProps({register: {...register, username: "foobar" }}) expect(_wrapper.find(TextInput).prop("value")).to.equal("foobar") }) it("Should call onChangeUsername.", () => { _spies.onChangeUsername.should.have.not.been.called _wrapper.find(TextInput).prop("onChange")("hello") _spies.dispatch.should.have.been.called }) }) beforeEach函數(shù)在每個(gè)測(cè)試用例啟動(dòng)前做一些初始化工作
enzyme shallow 的用法跟 jquery 的dom操作類(lèi)似,可以通過(guò)選擇器過(guò)濾出想要的節(jié)點(diǎn),可以接受 css 選擇器或者react class,如:find(".someClass"), find(TextInput)
這里用到了 sinon 的spies, 可以觀察到函數(shù)的調(diào)用情況。他還提供stub, mock功能,了解更多請(qǐng) google
四、action 的測(cè)試先來(lái)看一個(gè)普通的 action:
/* actions/register.js */ import * as Validator from "helpers/validator" export const CHANGE_USERNAME_ERROR = "CHANGE_USERNAME_ERROR" export function checkUsername (name) { return { type: CHANGE_USERNAME_ERROR, error: Validator.checkUsername(name) } }普通的 action 就是一個(gè)簡(jiǎn)單的函數(shù),返回一個(gè) object,測(cè)試起來(lái)跟前面的簡(jiǎn)單函數(shù)例子一樣:
/* tests/actions/register.js */ import * as Actions from "actions/register" describe("actions/register", () => { describe("Action: checkUsername", () => { it("Should export a constant CHANGE_USERNAME_ERROR.", () => { expect(Actions.CHANGE_USERNAME_ERROR).to.equal("CHANGE_USERNAME_ERROR") }) it("Should be exported as a function.", () => { expect(Actions.checkUsername).to.be.a("function") }) it("Should be return an action.", () => { const action = Actions.checkUsername("foobar") expect(action).to.have.property("type", Actions.CHANGE_USERNAME_ERROR) }) it("Should be return an action with error while input empty name.", () => { const action = Actions.checkUsername("") expect(action).to.have.property("error").to.not.be.empty }) }) })再來(lái)看一下異步 action, 這里功能是改變 username 的同時(shí)發(fā)起檢查:
export const CHANGE_USERNAME = "CHANGE_USERNAME" export function changeUsername (name) { return (dispatch) => { dispatch({ type: CHANGE_USERNAME, name }) dispatch(checkUsername(name)) } }測(cè)試代碼:
/* tests/actions/register.js */ import * as Actions from "actions/register" describe("actions/register", () => { let actions let dispatchSpy let getStateSpy beforeEach(function() { actions = [] dispatchSpy = sinon.spy(action => { actions.push(action) }) }) describe("Action: changeUsername", () => { it("Should export a constant CHANGE_USERNAME.", () => { expect(Actions.CHANGE_USERNAME).to.equal("CHANGE_USERNAME") }) it("Should be exported as a function.", () => { expect(Actions.changeUsername).to.be.a("function") }) it("Should return a function (is a thunk).", () => { expect(Actions.changeUsername()).to.be.a("function") }) it("Should be return an action.", () => { const action = Actions.checkUsername("foobar") expect(action).to.have.property("type", Actions.CHANGE_USERNAME_ERROR) }) it("Should call dispatch CHANGE_USERNAME and CHANGE_USERNAME_ERROR.", () => { Actions.changeUsername("hello")(dispatchSpy) dispatchSpy.should.have.been.calledTwice expect(actions[0]).to.have.property("type", Actions.CHANGE_USERNAME) expect(actions[0]).to.have.property("name", "hello") expect(actions[1]).to.have.property("type", Actions.CHANGE_USERNAME_ERROR) expect(actions[1]).to.have.property("error", "") }) }) })假如現(xiàn)在產(chǎn)品需求變更,要求實(shí)時(shí)在后臺(tái)檢查 username 的合法性, 就需要用到 ajax 了, 這里假設(shè)使用 Jquery 來(lái)實(shí)現(xiàn) ajax 請(qǐng)求:
/* actions/register.js */ export const CHANGE_USERNAME_ERROR = "CHANGE_USERNAME_ERROR" export function checkUsername (name) { return (dispatch) => { $.get("/check", {username: name}, (msg) => { dispatch({ type: CHANGE_USERNAME_ERROR, error: msg }) }) } }要測(cè)試 ajax 請(qǐng)求,可以用 sinon 的 fake XMLHttpRequest, 不用為了測(cè)試改動(dòng) action 任何代碼:
/* tests/actions/register.js */ import * as Actions from "actions/register" describe("actions/register", () => { let actions let dispatchSpy let getStateSpy let xhr let requests beforeEach(function() { actions = [] dispatchSpy = sinon.spy(action => { actions.push(action) }) xhr = sinon.useFakeXMLHttpRequest() requests = [] xhr.onCreate = function(xhr) { requests.push(xhr); }; }) afterEach(function() { xhr.restore(); }); describe("Action: checkUsername", () => { it("Should call dispatch CHANGE_USERNAME_ERROR.", () => { Actions.checkUsername("foo@bar")(dispatchSpy) const body = "不能含有特殊字符" // 手動(dòng)設(shè)置 ajax response requests[0].respond(200, {"Content-Type": "text/plain"}, body) expect(actions[0]).to.have.property("type", Actions. CHANGE_USERNAME_ERROR) expect(actions[0]).to.have.property("error", "不能含有特殊字符") }) }) })五、 reducer 的測(cè)試reducer 就是一個(gè)普通函數(shù) (state, action) => newState, 測(cè)試方法參考第三部分
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/80131.html
摘要:年終總結(jié)結(jié)果到這個(gè)時(shí)間才寫(xiě),其實(shí)也是無(wú)奈。這一年最重要的事情就是順利從一只學(xué)生狗轉(zhuǎn)職為一只社畜。四月份畢業(yè)之后以前端工程師的職位入職天貓,到現(xiàn)在也差不多工作一年了。 年終總結(jié)結(jié)果到這個(gè)時(shí)間才寫(xiě),其實(shí)也是無(wú)奈。本來(lái)計(jì)劃過(guò)年寫(xiě)的,沒(méi)想到Steam竟然開(kāi)了個(gè)農(nóng)歷春節(jié)特惠,然后就被各種游戲打了,辣雞平臺(tái),斂我錢(qián)財(cái),頹我精神,耗我青春,害我單身 以下全都是個(gè)人看法,如果有不認(rèn)同的地方,請(qǐng)大吼一聲...
摘要:年終總結(jié)結(jié)果到這個(gè)時(shí)間才寫(xiě),其實(shí)也是無(wú)奈。這一年最重要的事情就是順利從一只學(xué)生狗轉(zhuǎn)職為一只社畜。四月份畢業(yè)之后以前端工程師的職位入職天貓,到現(xiàn)在也差不多工作一年了。 年終總結(jié)結(jié)果到這個(gè)時(shí)間才寫(xiě),其實(shí)也是無(wú)奈。本來(lái)計(jì)劃過(guò)年寫(xiě)的,沒(méi)想到Steam竟然開(kāi)了個(gè)農(nóng)歷春節(jié)特惠,然后就被各種游戲打了,辣雞平臺(tái),斂我錢(qián)財(cái),頹我精神,耗我青春,害我單身 以下全都是個(gè)人看法,如果有不認(rèn)同的地方,請(qǐng)大吼一聲...
摘要:接下來(lái)演示不變性打開(kāi)終端并啟動(dòng)輸入。修改代碼如下我們使用在控制臺(tái)中打印出當(dāng)前的狀態(tài)??梢栽诳刂婆_(tái)中確認(rèn)新的商品已經(jīng)添加了。修改和文件最后,我們?cè)谥蟹职l(fā)這兩個(gè)保存完代碼之后,可以在瀏覽器的控制臺(tái)中檢查修改和刪除的結(jié)果。 典型的Web應(yīng)用程序通常由共享數(shù)據(jù)的多個(gè)UI組件組成。通常,多個(gè)組件的任務(wù)是負(fù)責(zé)展示同一對(duì)象的不同屬性。這個(gè)對(duì)象表示可隨時(shí)更改的狀態(tài)。在多個(gè)組件之間保持狀態(tài)的一致性會(huì)是一...
摘要:前言一直混跡社區(qū)突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來(lái)有點(diǎn)混亂所以將前端主流技術(shù)做了一個(gè)書(shū)簽整理不求最多最全但求最實(shí)用。 前言 一直混跡社區(qū),突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來(lái)有點(diǎn)混亂; 所以將前端主流技術(shù)做了一個(gè)書(shū)簽整理,不求最多最全,但求最實(shí)用。 書(shū)簽源碼 書(shū)簽導(dǎo)入瀏覽器效果截圖showImg(https://segmentfault.com/img/bVbg41b?w=107...
閱讀 3630·2023-04-25 23:32
閱讀 2044·2019-08-30 15:55
閱讀 2659·2019-08-30 15:52
閱讀 3115·2019-08-30 10:54
閱讀 844·2019-08-29 16:16
閱讀 655·2019-08-29 15:09
閱讀 3659·2019-08-26 14:05
閱讀 1640·2019-08-26 13:22