This article provides some ideas for unit testing the React family bucket application.
Construction guidelines
Jest
Jest is an open source JavaScript unit testing tool released by Facebook based on the Jasmine framework. It includes built-in DOM API support for Testing environments, assertion libraries, Mock libraries, and features such as Spapshot Testing and Instant Feedback.
Enzyme
Airbnb’s open source React test library Enzyme provides a set of simple and powerful APIS and DOM processing in a jquery-style way, making the development experience very friendly. Not only was it popular in the open source community, it was also featured by React.
redux-saga-test-plan
Redux-sag-test-plan runs in the JEST environment, simulates generator functions, and tests with mock data. Redux-sag-test-plan is a relatively friendly test scheme for Redux-Saga.
Start to prepare
Add the dependent
yarn add jest enzyme enzyme-adapter-react-16 enzyme-to-json redux-saga-test-plan@beta --dev
Copy the code
Description:
- The defaultAlready set upCan be used to
react
Test environment - As used in the project
react
The version is later than 16, so it needs to be installedenzyme
Adapter for this versionenzyme-adapter-react-16
enzyme-to-json
Used to serialize snapshots- Please note that,hole(Awkward self-answer).The documentNot to mention the
Story - saga1.0.0 - beta. 0
So if you follow the documentation instructions to install it, it will be available for testingrun
Anomalies, we are inissueTo discover the solution.
configuration
Add script commands to package.json
"scripts": {..."test": "jest"
}
Copy the code
Then go to configure jEST. Here are two configuration options:
- Directly in the
package.json
newjest
Attribute configuration
"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
}
Copy the code
- Create one in the root directory
xxx.js
File, added in the script command--config xxx.js
To informjest
Go to this file to read configuration information.
module.exports = {
... / / same as above
}
Copy the code
Description:
setupFiles
: Before each test file runs,Jest
The configuration file here is first run to initialize the specified test environmentmoduleFileExtensions
: Indicates the supported file typesnapshotSerializers
: Serializes snapshotstestPathIgnorePatterns
: re matches the test file to ignoremoduleNameMapper
: represents the need to beMock
Otherwise, an error will occur when the test script is run:.css
or.png
And so does not existidentity-obj-proxy
Development dependency)collectCoverage
: Specifies whether to generate a test coverage report--coverage
The preceding lists only some common configurations. For details, see the official documents.
The commencement of business
The React bucket test can be divided into three main sections.
Component test
// 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>
<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
Copy the code
// 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 = (a)= > {
/ / simulate the props
const props = {
type: 'activity'.activeTab: 'participant'.handleTabClick: jest.fn()
}
constsWrapper = 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) }) it("get child component's specific class", () => { // expect(sWrapper.find('.commentItem .text').length).toBe(1) // expect(sWrapper.find('.commentItem .text').exists()).toBeTruthy() // expect(sWrapper.find('.commentItem .text')).toHaveLength(1) expect(mWrapper.find('.commentItem .text').length).toBe(1) expect(sWrapper.find('.commentItem .text').length).toBe(1) }) 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() }) })Copy the code
Description:
test
Method isit
An alias for “, may be chosen according to personal habit;- You can find this by executing the script
shallow
withmount
Some of the differences:
shallow
Only render the current component, only make assertions on the current component, soexpect(sWrapper.find('.active').exists())
Normal andexpect(sWrapper.find('.commentItem .text').length).toBe(1)
The exception;mount
Renders the current component and all its children, so can be extended to assert its own components;enzyme
Another rendering method is also providedrender
, andshallow
andmount
Apply colours to a drawing givesreact
The tree is different. It rendershtml
thedom
Trees, and therefore it takes longer;
jest
Due to theSnapshot Testing
Feature, which will compare your last snapshot line by line, which is finePrevent inadvertent modification of componentsIn the operation.
Of course, you can also find more flexible test schemes in the Enzyme API Reference.
Saga test
// login.js part of the code
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')}}}}Copy the code
// 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': '[email protected]'.'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()
})
Copy the code
Description:
- contrast
saga
Code, combing script logic; expectSaga
Simplifies testing and provides us with examplesredux-saga
A style ofAPI
. Among themprovide
It liberates us tremendouslymock
Asynchronous data worries;- Of course, in
provide
In addition to usingmatchers
, can also be used directlyredux-saga/effects
, but note that if directly usedeffects
In thecall
Such methods will execute the method entity while usingmatchers
Do not. As shown in theStatic Providers;
- Of course, in
throwError
Throw the simulation wrong and entercatch
;
The selector test
// 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
}
)
Copy the code
// activity.test.js
import {
inSearchSelector,
channelsSelector,
channelsMapSelector
} from '.. /activity'
describe('activity selectors', () = > {let channels
describe('test simple selectors', () = > {let state
beforeEach((a)= > {
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((a)= > {
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)})})})})Copy the code
Description:
channelsMapSelector
You can call it a memory function, and it only triggers an update if its dependent value changes, which of course it canaccidentAnd theinSearchSelector
withchannelsSelector
Just two ordinary non-memoriesselector
Delta functions, they don’t transform themselect
The data;- If our
selector
More others are aggregated inselector
.resultFunc
Can help us mock data, no longer need fromstate
The corresponding data are obtained by the middle solution; recomputations
It helps us verify that the memory function really remembers;
Call it a day
Above, the understanding of their own simple description again, of course, there will be omissions or bias, hope to correct.
I did not write a complete front-end project unit test experience, just because the project needs to seriously learn again.
I hope you don’t have to go through it again.