Jest
是 Facebook 发布的一个开源的、基于 Jasmine
框架的 JavaScript
单元测试工具。提供了包括内置的测试环境 DOM API
支持、断言库、 Mock
库等,还包含了 Spapshot Testing
、 Instant Feedback
等特性。
Enzyme
Airbnb
开源的 React
测试类库 Enzyme
提供了一套简洁强大的 API
,并通过 jQuery
风格的方式进行 DOM
处理,开发体验十分友好。不仅在开源社区有超高人气,同时也获得了 React
官方的推荐。
redux-saga-test-plan
运行在 jest
环境下,模拟 generator
函数,使用 mock
数据进行测试,是对 redux-saga
比较友好的一种测试方案。
开工准备
添加依赖
yarn add jest enzyme enzyme-adapter-react-16 enzyme-to-json redux-saga-test-plan@beta --dev 复制代码
说明:
- 默认已经搭建好可用于
react
测试的环境 - 由于项目中使用
react
版本是在16以上,故需要安装enzyme
针对该版本的适配器enzyme-adapter-react-16
-
enzyme-to-json
用来序列化快照 - 请注意, 大坑 (尴尬的自问自答)。文档未提及对
redux-saga1.0.0-beta.0
的支持情况,所以如果按文档提示去安装则在测试时会有run
异常,我们在 issue 中发现解决方案。
配置
在 package.json
中新增脚本命令
"scripts": { ... "test": "jest" } 复制代码
然后再去对 jest
进行配置,以下是两种配置方案:
- 直接在
package.json
新增jest
属性进行配置
"jest": { "setupFiles": [ "./jestsetup.js" ], "moduleFileExtensions": [ "js", "jsx" ], "snapshotSerializers": [ "enzyme-to-json/serializer" ], "modulePaths": [ "<rootDir>/src" ], "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css|less|scss)$": "identity-obj-proxy" }, "testPathIgnorePatterns": [ '/node_modules/', "helpers/test.js" ], "collectCoverage": false } 复制代码
- 根目录下新建
xxx.js
文件,在脚本命令中添加--config xxx.js
,告知jest
去该文件读取配置信息。
module.exports = { ... // 同上 } 复制代码
说明:
-
setupFiles
:在每个测试文件运行前,Jest
会先运行这里的配置文件来初始化指定的测试环境 -
moduleFileExtensions
:支持的文件类型 -
snapshotSerializers
: 序列化快照 -
testPathIgnorePatterns
:正则匹配要忽略的测试文件 -
moduleNameMapper
:代表需要被Mock
的文件类型,否则在运行测试脚本的时候常会报错:.css
或.png
等不存在 (如上需要添加identity-obj-proxy
开发依赖) -
collectCoverage
:是否生成测试覆盖报告,也可以在脚本命令后添加--coverage
以上仅列举了部分常用配置,更多详见官方文档。
开工大吉
对 React
应用全家桶的测试主要可分为三大块。
组件测试
// Tab.js import React from 'react' import PropTypes from 'prop-types' import TabCell from './TabCell' import styles from './index.css' const Tab = ({ type, activeTab, likes_count: liked, goings_count: going, past_count: past, handleTabClick }) => { return (<div className={styles.tab}> {type === 'user' ? <div> ![](https://user-gold-cdn.xitu.io/2018/11/1/166cf21e3bffb7d7?w=1484&h=1442&f=png&s=287585) <TabCell type='liked' text={`${liked} Likes`} isActived={activeTab === 'liked'} handleTabClick={handleTabClick} /> <TabCell type='going' text={`${going} Going`} isActived={activeTab === 'going'} handleTabClick={handleTabClick} /> <TabCell type='past' text={`${past} Past`} isActived={activeTab === 'past'} handleTabClick={handleTabClick} /> </div> : <div> <TabCell type='details' text='Details' isActived={activeTab === 'details'} handleTabClick={handleTabClick} /> <TabCell type='participant' text='Participant' isActived={activeTab === 'participant'} handleTabClick={handleTabClick} /> <TabCell type='comment' text='Comment' isActived={activeTab === 'comment'} handleTabClick={handleTabClick} /> </div> } </div>) } Tab.propTypes = { type: PropTypes.string, activeTab: PropTypes.string, likes_count: PropTypes.number, goings_count: PropTypes.number, past_count: PropTypes.number, handleTabClick: PropTypes.func } export default Tab 复制代码
// Tab.test.js import React from 'react' import { shallow, mount } from 'enzyme' import renderer from 'react-test-renderer' import Tab from 'components/Common/Tab' import TabCell from 'components/Common/Tab/TabCell' const setup = () => { // 模拟props const props = { type: 'activity', activeTab: 'participant', handleTabClick: jest.fn() } const sWrapper = shallow(<Tab {...props} />) const mWrapper = mount(<Tab {...props} />) return { props, sWrapper, mWrapper } } describe('Tab components', () => { const { sWrapper, mWrapper, props } = setup() it("get child component TabCell's length", () => { expect(sWrapper.find(TabCell).length).toBe(3) expect(mWrapper.find(TabCell).length).toBe(3) }) test('get specific class', () => { expect(sWrapper.find('.active').exists()) expect(mWrapper.find('.active').exists()) }) it("get child component's specific class", () => { expect(mWrapper.find('.commentItem .text').length).toBe(1) expect(sWrapper.find('.commentItem .text').length).toBe(1) }) test('shallowWrapper function to be called', () => { sWrapper.find('.active .text').simulate('click') expect(props.handleTabClick).toBeCalled() }) test('mountWrapper function to be called', () => { mWrapper.find('.active .text').simulate('click') expect(props.handleTabClick).toBeCalled() }) it('set props', () => { expect(mWrapper.find('.participantItem.active')).toHaveLength(1) mWrapper.setProps({activeTab: 'details'}) expect(mWrapper.find('.detailsItem.active')).toHaveLength(1) }) // Snapshot it('Snapshot', () => { const tree = renderer.create(<Tab {...props} />).toJSON() expect(tree).toMatchSnapshot() }) }) 复制代码
说明:
-
test
方法是it
的一个别名,可以根据个人习惯选用; - 执行脚本可以发现
shallow
与mount
的些些区别:-
shallow
只渲染当前组件,只能对当前组件做断言,所以expect(sWrapper.find('.active').exists())
正常而expect(mWrapper.find('.commentItem .text').length).toBe(1)
异常; -
mount
会渲染当前组件以及所有子组件,故而可以扩展到对其自组件做断言; -
enzyme
还提供另外一种渲染方式render
,与shallow
及mount
渲染出react
树不同,它的渲染结果是html
的dom
树,也因此它的耗时也较长;
-
-
jest
因Snapshot Testing
特性而备受关注,它将逐行比对你上一次建的快照,这可以很好的 防止无意间修改组件 的操作。
当然,你还可以在 enzyme
的API Reference找到更多灵活的测试方案。
saga测试
// login.js部分代码 export function * login ({ payload: { params } }) { yield put(startSubmit('login')) let loginRes try { loginRes = yield call(fetch, { ssl: false, method: 'POST', version: 'v1', resource: 'auth/token', payload: JSON.stringify({ ...params }) }) const { token, user: currentUser } = loginRes yield call(setToken, token) yield put(stopSubmit('login')) yield put(reset('login')) yield put(loginSucceeded({ token, user: currentUser })) const previousUserId = yield call(getUser) if (previousUserId && previousUserId !== currentUser.id) { yield put(reduxReset()) } yield call(setUser, currentUser.id) if (history.location.pathname === '/login') { history.push('/home') } return currentUser } catch (e) { if (e.message === 'error') { yield put(stopSubmit('login', { username: [{ code: 'invalid' }] })) } else { if (e instanceof NotFound) { console.log('notFound') yield put(stopSubmit('login', { username: [{ code: 'invalid' }] })) } else if (e instanceof Forbidden) { yield put(stopSubmit('login', { password: [{ code: 'authorize' }] })) } else if (e instanceof InternalServerError) { yield put(stopSubmit('login', { password: [{ code: 'server' }] })) } else { if (e.handler) { yield call(e.handler) } console.log(e) yield put(stopSubmit('login')) } } } } 复制代码
// login.test.js import {expectSaga} from 'redux-saga-test-plan' import * as matchers from 'redux-saga-test-plan/matchers' import { throwError } from 'redux-saga-test-plan/providers' import {loginSucceeded, login} from '../login' import fetch from 'helpers/fetch' import { startSubmit, stopSubmit, reset } from 'redux-form' import { setToken, getUser, setUser } from 'services/authorize' const params = { username: 'yy', password: '123456' } it('login maybe works', () => { const fakeResult = { 'token': 'd19911bda14cb0f36b82c9c6f6835c8c', 'user': { 'id': 53, 'username': 'yy', 'email': 'yan.yang@shopee.com', 'avatar': 'https://coding.net/static/fruit_avatar/Fruit-19.png' } } return expectSaga(login, { payload: { params } }) .put(startSubmit('login')) .provide([ [matchers.call.fn(fetch), fakeResult], [matchers.call.fn(setToken), fakeResult.token], [matchers.call.fn(getUser), 53], [matchers.call.fn(setUser), 53] ]) .put(stopSubmit('login')) .put(reset('login')) .put(loginSucceeded({ token: fakeResult.token, user: fakeResult.user })) .returns({...fakeResult.user}) .run() }) it('catch an error', () => { const error = new Error('error') return expectSaga(login, { payload: { params } }) .put(startSubmit('login')) .provide([ [matchers.call.fn(fetch), throwError(error)] ]) .put(stopSubmit('login', { username: [{ code: 'invalid' }] })) .run() }) 复制代码
说明:
- 对照
saga
代码,梳理脚本逻辑(可以只编写对核心逻辑的断言); -
expectSaga
简化了测试,为我们提供了如redux-saga
风格般的API
。其中provide
极大的解放了我们mock
异步数据的烦恼;- 当然,在
provide
中除了使用matchers
,也可以直接使用redux-saga/effects
中的方法,不过注意如果直接使用effects
中的call
等方法将会执行该方法实体,而使用matchers
则不会。详见Static Providers;
- 当然,在
-
throwError
将模拟抛错,进入到catch
中;
selector测试
// activity.js import { createSelector } from 'reselect' export const inSearchSelector = state => state.activityReducer.inSearch export const channelsSelector = state => state.activityReducer.channels export const channelsMapSelector = createSelector( [channelsSelector], (channels) => { const channelMap = {} channels.forEach(channel => { channelMap[channel.id] = channel }) return channelMap } ) 复制代码
// activity.test.js import { inSearchSelector, channelsSelector, channelsMapSelector } from '../activity' describe('activity selectors', () => { let channels describe('test simple selectors', () => { let state beforeEach(() => { channels = [{ id: 1, name: '1' }, { id: 2, name: '2' }] state = { activityReducer: { inSearch: false, channels } } }) describe('test inSearchSelector', () => { it('it should return search state from the state', () => { expect(inSearchSelector(state)).toEqual(state.activityReducer.inSearch) }) }) describe('test channelsSelector', () => { it('it should return channels from the state', () => { expect(channelsSelector(state)).toEqual(state.activityReducer.channels) }) }) }) describe('test complex selectors', () => { let state const res = { 1: { id: 1, name: '1' }, 2: { id: 2, name: '2' } } const reducer = channels => { return { activityReducer: {channels} } } beforeEach(() => { state = reducer(channels) }) describe('test channelsMapSelector', () => { it('it should return like res', () => { expect(channelsMapSelector(state)).toEqual(res) expect(channelsMapSelector.resultFunc(channels)) }) it('recoputations count correctly', () => { channelsMapSelector(state) expect(channelsMapSelector.recomputations()).toBe(1) state = reducer([{ id: 3, name: '3' }]) channelsMapSelector(state) expect(channelsMapSelector.recomputations()).toBe(2) }) }) }) }) 复制代码
说明:
-
channelsMapSelector
可以称之为记忆函数,只有当其依赖值发生改变时才会触发更新,当然也可能会发生 意外 ,而inSearchSelector
与channelsSelector
仅仅是两个普通的非记忆selector
函数,并没有变换他们select
的数据; - 如果我们的
selector
中聚合了比较多其他的selector
,resultFunc
可以帮助我们mock数据,不需要再从state
中解藕出对应数据; -
recomputations
帮助我们校验记忆函数是否真的能记忆;
收工
以上,把自己的理解都简单的描述了一遍,当然肯定会有缺漏或者偏颇,望指正。
没有完整的写过前端项目单元测试的经历,刚好由于项目需要便认真去学习了一遍。
其中艰辛,希望众位不要再经历了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- vue全家桶
- 升级vue全家桶过程记录
- SpringBootBucket 1.0.0 发布,SprintBoot全家桶
- 免费获取 JetBrains 全家桶正版 License 教程
- 使用React全家桶搭建一个后台管理系统
- Windows 10四月更新恢复预装“全家桶”遭吐槽
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
An Introduction to the Analysis of Algorithms
Robert Sedgewick、Philippe Flajolet / Addison-Wesley Professional / 1995-12-10 / CAD 67.99
This book is a thorough overview of the primary techniques and models used in the mathematical analysis of algorithms. The first half of the book draws upon classical mathematical material from discre......一起来看看 《An Introduction to the Analysis of Algorithms》 这本书的介绍吧!