preface

Testing is an indispensable link in the process of application production. Developers always have inconsiderate or wrong situations when coding, and testing is to find out problems and defects by comparing the actual results with the expected results, so as to ensure the quality of software. Use Jest and Enzyme to test React components.

Type test

For a Web site, there are three main types of testing:

  • Unit testing: Testing a single function or class to provide input and make sure the output is as expected. Unit tests should be as small in granularity as possible, leaving out implementations of other classes and modules.
  • Integration testing: Testing that an entire process or component works as expected to cover processes across modules. Also include some negative use cases.
  • Functional testing: Test scenarios from the perspective of the product, by manipulating a browser or website, ignoring internal implementation details and structure, to make sure it behaves as expected.

The test framework

There are a lot of test tools on the market. Umijs is used as the scaffolding to build a React application, while Umi uses Dva as the data flow management, and also automatically configure the Jest test framework.

Recommended by Facebook, Jest testing framework has the advantages of fast running and good performance, and covers a variety of functional modules required for testing (including assertions, simulation functions, snapshot comparison of components, running tests, generating test results and coverage, etc.). It is easy to configure and suitable for rapid testing of large projects.

Test the React component

To test the React component, use the Enzyme library, which provides three component rendering methods:

  1. Shallow: Does not render child components
  2. MountRender child components, including life cycle functions such ascomponentDidMount
  3. RenderRender subcomponents, but do not include life cycles, and the available apis are reduced for examplesetState()

Shallow and mount are generally used more often.

A component wrapped with Connect

Some components are wrapped by Connect, which cannot be measured directly. It is necessary to create a Provider and pass in a store, which is a painful process. It is better to export the component after removing Connect and test it separately, adopt a shallow rendering method, and only test the logic of the component.

For example, the components to be tested are as follows:

export class Dialog extends Component {
    ...
}
export default connect(mapStateToProps, mapDispatch)(Dialog)
Copy the code

So in the test file, you can initialize a control like this:

import {Dialog} from '.. /dialog'
functionsetup(propOverrides) { const props = Object.assign( { state:{} actions:{}, }, propOverrides, ) const enzymeWrapper = shallow(<Dialog {... props} />)return {
    props,
    enzymeWrapper,
  }
}
Copy the code

Components that interact with child components and native DOM elements

Some components need to test interaction with native DOM elements, such as clicking on a native button element to see if the current component’s event is triggered, or to test interaction with child components.

For example, my Editor component looks like this:

export default class Editor extends Component {
  constructor(props) {
    super(props)
    this.state = {
      onClickBtn: null,
    }
  }
  handleSubmit = ({ values, setSubmitting }) => {
    const { onClickBtn } = this.state
    this.props.actions.createInfo(values, onClickBtn)
  }
  handleCancel = () => {
    ...
  }
  setOnClickBtn(name) {
    this.setState({
      onClickBtn: name,
    })
  }
  render() {
    return (
      <Form onSubmit={this.handleSubmit}>
        {({ handleChange }) => {
          return (
            <div className="information-form"> <Input name={FIELD_ROLE_NAME} onChange={handleChange} /> <Input name={FIELD_ROLE_KEY} onChange={handleChange} /> <div>  <Buttontype="button" onClick={this.handleCancel}> Cancel </Button>
                <Button type="submit" primary onClick={() => this.setOnClickBtn('create')} > Create </Button>
                <Button type="submit" primary onClick={() => this.setOnClickBtn('continue')} > Create and Continue </Button>}
              </div>
            </div>
          )
        }}
      </Form>
    )
  }
}
Copy the code

In this case, the children of the Form is a function. To test the button click event in the Form, the children element in the Form cannot be found if only shallow is used. Therefore, the whole DOM is rendered in mount mode. Can directly simulate the button click event whose type is submit attribute. Then test whether clicking the button completes two events: handleSubmit and setOnclickBtn.

One might think of a Submit event that simulates a Form, but in the case of a mount, the Click event that simulates a Button can also trigger an onSubmit event.

As the submit process involves the interaction of sub-controls, the process has certain uncertainty. At this time, it is necessary to set a timeout to extend a period of time to judge whether the action in submit is executed.

it('should call create role action when click save', () => {
    const preProps = {
      actions: {
        createInfo: jest.fn(),
      }
    }
    const { props, enzymeWrapper } = setup(preProps)
    const nameInput = enzymeWrapper.find('input').at(0)
    nameInput.simulate('change', { target: { value: 'RoleName' } })

    const keyInput = enzymeWrapper.find('input').at(1)
    keyInput.simulate('change', { target: { value: 'RoleKey' } })

    const saveButton = enzymeWrapper.find('button[type="submit"]').at(0)
    saveButton.simulate('click')
    expect(enzymeWrapper.state().onClickBtn).toBe('save')
    setTimeout(() => {
      expect(props.actions.createInfo).toHaveBeenCalled()
    }, 500)
  })
Copy the code

But there are pitfalls with mount rendering, such as finding child components that may require multiple layers of.children(). In unit tests, shallow rendering should be used as far as possible and test granularity should be minimized.

Cases with promises

Function logic of some components will contain a Promise, the results returned with uncertainty, for example the following code segment in the auth. HandleAuthenticateResponse, incoming argument is a callback function, Need according to the auth. HandleAuthenticateResponse processing result is error is normal result to handle its own internal logic.

 handleAuthentication = () => {
    const { location, auth } = this.props
    if (/access_token|id_token|error/.test(location.search)) {
      auth.handleAuthenticateResponse(this.handleResponse)
    }
  }

  handleResponse = (error, result) => {
    const { auth } = this.props
    let postMessageBody = null
    if (error) {
      postMessageBody = error
    } else {
      auth.setSession(result)
      postMessageBody = result
    }
    this.handleLogicWithState(postMessageBody)
  }
Copy the code

In the test, the available jest. Fn () simulate the auth. HandleAuthenticateResponse function, at the same time make it return a certain result.

const preProps = {
  auth: {
    handleAuthenticateResponse: jest.fn(cb => cb(errorMsg))
  }
}
setup(preProps)
Copy the code

The relevant API

enzyme: airbnb.io/enzyme/

Jest: jestjs.io/docs/en/api

Retention problems

  1. When using mount to test a parent component that contains children, and the interaction between the parent and child components, is this called a UT test or a CT component test?
  2. What are component Snapshot tests? Is it necessary? How to measure?