The simplest test

Jest is a test tool developed by Facebook, and enzyme is a test library developed by Airbnb. You can use both to test the React component.

First install the corresponding dependency:

npm i -D jest babel-jest @babel/core @babel/preset-env @babel/preset-react 
Copy the code

Where babel-jest is used to compile files automatically using Babel.

Install enzyme related dependencies:

npm i -D enzyme enzyme-adapter-react-16 jest-environment-enzyme
Copy the code

Configure the enzyme, create a new file setupenzyme. js, and write the following:

import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

Enzyme.configure({
  adapter: new Adapter()
})
Copy the code

Configure the corresponding JEST,

{
  "setupFilesAfterEnv": [
    "./setupEnzyme.js"]."testEnvironment": "enzyme"
}
Copy the code

Configure the corresponding Babel:

{
  "presets": [["@babel/preset-env"],
    ["@babel/preset-react"]]}Copy the code

Configure NPM scripts:

"scripts": {
    "test": "jest --config=jest.config.json"
  },
Copy the code

Take the simplest list component as an example:

import React from 'react'

export default ({list}) => {
  return <ul>
     {
       list.map(item => item > 5 && <li className="item" key={item}>{item}</li>)}</ul>
}
Copy the code

For example, passing a list of data [1,2,3,4,5,6] would render one list item, while passing data [6,7,8,9] would render four list items, as follows:

import React from 'react'
import { render } from 'enzyme'

import List from './index'

describe('<List/>', () => {
  it('render 1 child', () = > {constWrapper = render(<List List ={[1,2,3,4,5,6]} />) expect(wrapper. Find ('.item').length).tobe (1)}) it('render 4 child', () = > {const wrapper = render (< List List = {,7,8,9 [6]} / >) expect (wrapper. Find (' item '). The length), place (4)})})Copy the code

Run the test command to see the screen where the test passes.

With redux

An easy way to unit test connected components with Redux is to export the plain component as well, as follows:

import React from 'react'
import { connect } from 'react-redux'

export const List = ({list}) = > {
  return <ul>
     {
       list.map(item => item > 5 && <li className="item" key={item}>{item}</li>)}</ul>
}

export default connect((state) = >({list:state.list}))(List)
Copy the code

So you can test it just like you did the previous test.

Another way to do this is to use redux-mock-store. Once installed, use a simple example like this:

import React from 'react'
import { mount } from 'enzyme'
import configureStore from 'redux-mock-store'
import { Provider } from 'react-redux'
import List from './index'


const mockStore = configureStore([])

describe('<List/>', () => {
  it('render 0 child', () = > {const store = mockStore({
      list: []})const wrapper = mount(<Provider store={store}><List/></Provider>)
    expect(wrapper.find('.item').length).toBe(0)
  })

  it('render 2 child', () = > {const store = mockStore({
      list: ['111'.'222']})const wrapper = mount(<Provider store={store}><List/></Provider>)
    expect(wrapper.find('.item').length).toBe(2)})})Copy the code

Two things to note here:

  • redux-mock-storeJust to test the logic related to actions, the store is not automatically updated (so the second test above is not used directlystore.dispatchThe reducer is a pure function, and the reducer should be tested as it should be. SeeGithub.com/arnaudbenar…)
  • The second is usemountInstead of usingshallowbecauseshallowRender only the current component, can only make assertions on the current component;mountRenders the current component and all its children, which is obviously needed abovemount

with state

Unit testing should not test the implementation logic, but should focus on the input and output, and changes in state are basically reflected in changes in the UI, so just focus on changes in THE UI. In the following example, the state content changes after the button is clicked:

import React, { useState } from 'react'

const Add = (a)= > {
  const [text, setText] = useState('hello')
  return (
    <div>
      <button onClick={()= > setText(text === 'hello' ? 'world' : 'hello')}>change</button>
      <p>{text}</p>
    </div>)}export default Add
Copy the code

Then his test should be to test whether the text content changes after clicking, as follows:

import React from 'react'
import { shallow } from 'enzyme'

import Add from './index'

describe('<Add/>', () => {
  it('click', () = > {const wrapper = shallow(<Add/>)
    expect(wrapper.find('p').text()).toBe('hello')
    wrapper.find('button').simulate('click')
    expect(wrapper.find('p').text()).toBe('world')
    wrapper.find('button').simulate('click')
    expect(wrapper.find('p').text()).toBe('hello')})})Copy the code

The UI interaction

For user interface operations, enzymes can simulate interactive events by using SIMULATE. In a simple example, a user clicks a button and triggers an event to update the store:

import React from 'react'
import { mount } from 'enzyme'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'

import Add from './index'

const mockStore = configureStore([])

describe('<Add/>', () => {
  it('click', () = > {const store = mockStore({
      list: []})const wrapper = mount(<Provider store={store}><Add/></Provider>)

    wrapper.find('button').simulate('click')
    wrapper.find('button').simulate('click')

    expect(store.getActions().length).toBe(2)})})Copy the code

In this case, the user clicks twice to trigger two dispatches, and determines whether dispatch is triggered and the number of dispatches through store.getActions.

snapshot

The first time you run the Snapshot test, you save a snapshot file of the React component rendering results under different conditions. To generate a new snapshot file, add the -u parameter to generate a new snapshot file. Snapshot files end in.snap and are stored in the __snapshots__ folder when the tests are run.

The snapshot test jest provides the react-test-renderer, the enzyme render provides the wrapper, and the enzyme-to-json helps compare the wrapper to the snapshot file. Here’s a simple example:

it('basic use', () = > {const text = ['12'.'13']

    const wrapper = render(
      <List list={text} />
    )

    expect(toJson(wrapper)).toMatchSnapshot()
  })

  it('without item', () => {
    const wrapper = render(
      <List list={[]}/>
    )

    expect(toJson(wrapper)).toMatchSnapshot()
  })
Copy the code

The snapshot file is generated after the test is run for the first time:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<List/> basic use 1`] = ` 
      
  • 12
  • 13
`
; exports[`<List/> without item 1`] = `<ul />`; Copy the code

After modifying the component, re-testing will report an error:

If you are sure that the changes are what you expect, then you should regenerate the snapshot file.

Should snapshot files be commit tracked by Git? Of course, snapshot files should be submitted and reviewed along with the code.

Snapshot file update problem, if a period of time a lot of changes a lot of UI components, console at this time there will be a lot of mistakes, this time need to separate each component is in compliance with our changes, in line with the words you need to regenerate the snapshot, -u parameter is the default update all snapshots, if just want to update, You can use the –testNamePattern parameter.

Commit git as much as you can and put single tests on Git hooks to avoid having to review too many files at once.

with TypeScript

Migrate to Typescript and replace babel-jest with ts-jest.

npm i ts-jest -D
Copy the code

Reference:

  • Use jest+enzyme to test react projects
  • enzyme_render_diffs.md
  • Use Enzyme to test the React component further
  • Use jest+enzyme to write Reactjs unit tests.
  • Testing React / Redux Apps with Jest & Enzyme

Finally, it is an advertisement. Recently, a new public account for sharing technology has been opened, and you are welcome to pay attention to 👇 (currently the number of followers is poor 🤕).