React+Redux单元测试一小时入门
一、工具介绍
二、环境准备工具安装就是 { 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可以多次嵌套使用,更清晰的描述测试功能的结构。执行单元测试:
三、component测试在 redux 的理念中,react 组件应该分为视觉组件 component 和 高阶组件 container, /* 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 <div style={{padding: '50px 130px'}}> <FormGroup> <FormLabel>用户名</FormLabel> <FormControl> <TextInput width='370px' limit={15} value={register.username} onChange={onChangeUsername} /> <FormTip>请输入用户名</FormTip> <FormError>{register.usernameError}</FormError> </FormControl> </FormGroup> <FormGroup> <Button type='primary' onClick={onSubmit}>提交</Button> </FormGroup> </div> } } 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) 测试 componet,这里用到 import React from 'react' import { bindActionCreators } from 'redux' import { Register } from 'components/Register' import { shallow } from 'enzyme' import { FormGroup,Dropdown,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(<Register {..._props} />) }) it('Should render as a <div>.',() => { 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 <FormGroup>.',() => { _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 }) })
这里用到了 四、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.',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','') }) }) }) 假如现在产品需求变更,要求实时在后台检查 /* 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 请求,可以用 /* 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 就是一个普通函数 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |