摘要:可以監控文件變化自動執行單元測試,可以緩存測試結果,可以顯示測試過程中的變量測試框架。執行單元測試三測試在的理念中,組件應該分為視覺組件和高階組件,與邏輯分離,更利于測試。
一、工具介紹
karma:測試過程管理工具。可以監控文件變化自動執行單元測試,可以緩存測試結果,可以console.log顯示測試過程中的變量
mocha:測試框架。提供describe,it,beforeEach等函數管理你的 testcase,后面示例中會看到
chai:BDD(行為驅動開發)和TDD(測試驅動開發)雙測試風格的斷言庫
enzyme:React測試工具,可以類似 jquery 風格的 api 操作react 節點
sinon: 提供 fake 數據, 替換函數調用等功能
二、環境準備工具安裝就是 npm install,這里就不再詳述,主要的配置項目在karma.conf.js中,可以參考這個模板項目 react-redux-starter-kit 。如果項目中用到全局變量,比如jquery, momentjs等,需要在測試環境中全局引入,否則報錯,例如,在karma.conf中引入全局變量jQuery:
{ files: [ "./node_modules/jquery/jquery.min.js", { pattern: `./tests/test-bundler.js`, watched: false, served: true, included: true } ] }
在test-bundler.js中設置全局的變量,包括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() ...三、簡單的函數測試
先熱身看看簡單的函數如何單元測試:
/* helpers/validator.js */ export function checkUsername (name) { if (name.length === 0 || name.length > 15) { return "用戶名必須為1-15個字" } 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("用戶名必須為1-15個字") }) it("Should return error while more then 15 words.", () => { expect(Validators.checkUsername("abcdefghijklmnop")).to.equal("用戶名必須為1-15個字") expect(Validators.checkUsername("一二三四五六七八九十一二三四五六")).to.equal("用戶名必須為1-15個字") }) }) })
describe可以多次嵌套使用,更清晰的描述測試功能的結構。執行單元測試:
babel-node ./node_modules/karma/bin/karma start build/karma.conf
在 redux 的理念中,react 組件應該分為視覺組件 component 和 高階組件 container,UI與邏輯分離,更利于測試。redux 的 example 里,這兩種組件一般都分開文件去存放。本人認為,如果視覺組件需要多次復用,應該與container分開來寫,但如果基本不復用,或者可以復用的組件已經專門組件化了(下面例子就是),那就沒必要分開寫,可以寫在一個文件里更方便管理,然后通過 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)用戶名 請輸入用戶名 {register.usernameError}
測試 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函數在每個測試用例啟動前做一些初始化工作
enzyme shallow 的用法跟 jquery 的dom操作類似,可以通過選擇器過濾出想要的節點,可以接受 css 選擇器或者react class,如:find(".someClass"), find(TextInput)
這里用到了 sinon 的spies, 可以觀察到函數的調用情況。他還提供stub, mock功能,了解更多請 google
四、action 的測試先來看一個普通的 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 就是一個簡單的函數,返回一個 object,測試起來跟前面的簡單函數例子一樣:
/* 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 }) }) })再來看一下異步 action, 這里功能是改變 username 的同時發起檢查:
export const CHANGE_USERNAME = "CHANGE_USERNAME" export function changeUsername (name) { return (dispatch) => { dispatch({ type: CHANGE_USERNAME, name }) dispatch(checkUsername(name)) } }測試代碼:
/* 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", "") }) }) })假如現在產品需求變更,要求實時在后臺檢查 username 的合法性, 就需要用到 ajax 了, 這里假設使用 Jquery 來實現 ajax 請求:
/* 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 }) }) } }要測試 ajax 請求,可以用 sinon 的 fake XMLHttpRequest, 不用為了測試改動 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 = "不能含有特殊字符" // 手動設置 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 的測試reducer 就是一個普通函數 (state, action) => newState, 測試方法參考第三部分
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/80131.html
摘要:年終總結結果到這個時間才寫,其實也是無奈。這一年最重要的事情就是順利從一只學生狗轉職為一只社畜。四月份畢業之后以前端工程師的職位入職天貓,到現在也差不多工作一年了。 年終總結結果到這個時間才寫,其實也是無奈。本來計劃過年寫的,沒想到Steam竟然開了個農歷春節特惠,然后就被各種游戲打了,辣雞平臺,斂我錢財,頹我精神,耗我青春,害我單身 以下全都是個人看法,如果有不認同的地方,請大吼一聲...
摘要:年終總結結果到這個時間才寫,其實也是無奈。這一年最重要的事情就是順利從一只學生狗轉職為一只社畜。四月份畢業之后以前端工程師的職位入職天貓,到現在也差不多工作一年了。 年終總結結果到這個時間才寫,其實也是無奈。本來計劃過年寫的,沒想到Steam竟然開了個農歷春節特惠,然后就被各種游戲打了,辣雞平臺,斂我錢財,頹我精神,耗我青春,害我單身 以下全都是個人看法,如果有不認同的地方,請大吼一聲...
摘要:接下來演示不變性打開終端并啟動輸入。修改代碼如下我們使用在控制臺中打印出當前的狀態。可以在控制臺中確認新的商品已經添加了。修改和文件最后,我們在中分發這兩個保存完代碼之后,可以在瀏覽器的控制臺中檢查修改和刪除的結果。 典型的Web應用程序通常由共享數據的多個UI組件組成。通常,多個組件的任務是負責展示同一對象的不同屬性。這個對象表示可隨時更改的狀態。在多個組件之間保持狀態的一致性會是一...
摘要:前言一直混跡社區突然發現自己收藏了不少好文但是管理起來有點混亂所以將前端主流技術做了一個書簽整理不求最多最全但求最實用。 前言 一直混跡社區,突然發現自己收藏了不少好文但是管理起來有點混亂; 所以將前端主流技術做了一個書簽整理,不求最多最全,但求最實用。 書簽源碼 書簽導入瀏覽器效果截圖showImg(https://segmentfault.com/img/bVbg41b?w=107...
閱讀 3617·2023-04-25 23:32
閱讀 2039·2019-08-30 15:55
閱讀 2651·2019-08-30 15:52
閱讀 3110·2019-08-30 10:54
閱讀 839·2019-08-29 16:16
閱讀 646·2019-08-29 15:09
閱讀 3647·2019-08-26 14:05
閱讀 1632·2019-08-26 13:22