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 toreactTest environment
  • As used in the projectreactThe version is later than 16, so it needs to be installedenzymeAdapter for this versionenzyme-adapter-react-16
  • enzyme-to-jsonUsed to serialize snapshots
  • Please note that,hole(Awkward self-answer).The documentNot to mention theStory - saga1.0.0 - beta. 0So if you follow the documentation instructions to install it, it will be available for testingrunAnomalies, 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 thepackage.jsonnewjestAttribute 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 directoryxxx.jsFile, added in the script command--config xxx.jsTo informjestGo to this file to read configuration information.
module.exports = {
    ... / / same as above
}

Copy the code

Description:

  • setupFiles: Before each test file runs,JestThe configuration file here is first run to initialize the specified test environment
  • moduleFileExtensions: Indicates the supported file type
  • snapshotSerializers: Serializes snapshots
  • testPathIgnorePatterns: re matches the test file to ignore
  • moduleNameMapper: represents the need to beMockOtherwise, an error will occur when the test script is run:.cssor.pngAnd so does not existidentity-obj-proxyDevelopment 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:

  • testMethod isitAn alias for “, may be chosen according to personal habit;
  • You can find this by executing the scriptshallowwithmountSome of the differences:
    • shallowOnly 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;
    • mountRenders the current component and all its children, so can be extended to assert its own components;
    • enzymeAnother rendering method is also providedrender, andshallowandmountApply colours to a drawing givesreactThe tree is different. It rendershtmlthedomTrees, and therefore it takes longer;
  • jestDue to theSnapshot TestingFeature, 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:

  • contrastsagaCode, combing script logic;
  • expectSagaSimplifies testing and provides us with examplesredux-sagaA style ofAPI. Among themprovideIt liberates us tremendouslymockAsynchronous data worries;
    • Of course, inprovideIn addition to usingmatchers, can also be used directlyredux-saga/effects, but note that if directly usedeffectsIn thecallSuch methods will execute the method entity while usingmatchersDo not. As shown in theStatic Providers;
  • throwErrorThrow 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:

  • channelsMapSelectorYou can call it a memory function, and it only triggers an update if its dependent value changes, which of course it canaccidentAnd theinSearchSelectorwithchannelsSelectorJust two ordinary non-memoriesselectorDelta functions, they don’t transform themselectThe data;
  • If ourselectorMore others are aggregated inselector.resultFuncCan help us mock data, no longer need fromstateThe corresponding data are obtained by the middle solution;
  • recomputationsIt 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.