The official documentation

jest

enzyme

The class components

import React from 'react' import { Button, Space} from 'antd' import { ReloadOutlined } from '@ant-design/icons' export default class CountDownButton extends React.Component { constructor(proos) { super(proos); this.state = { isRun: false, time: 60 } } render() { const { isRun, time } = this.state; const { style, type, size } = this.props; return ( isRun ? ( <Button style={style} type={type} size={size} disabled={true} >{`${time} Get Latest Data`} </Button>) : ( <Button style={style} type={type} size={size} icon={<ReloadOutlined />} onClick={this.onClickRefresh} >{`Get Latest Data`} </Button> ) ) } onClickRefresh = () => { this.setState({ isRun: true, time: 60 }, () => { this.props.onClick? .() this.timer && clearInterval(this.timer); this.timer = setInterval(() => { if (this.state.time == 1) { clearInterval(this.timer); this.setState({ isRun: false }); } else { this.setState({ time: this.state.time - 1 }); }}, 1000); }); } componentWillUnmount() { this.timer && clearInterval(this.timer); }}Copy the code

The test case

import React from 'react' import { shallow, configure } from 'enzyme' import CountDownButton from './CountDownButton' import Adapter from 'enzyme-adapter-react-16' import renderer from 'react-test-renderer' const setup = () => { return ( <CountDownButton /> ) } describe('>>>CountDownButton --- event', () => { jest.useFakeTimers() it('+++ onClick', () => { let wrapper = shallow(setup()) let instance = wrapper.instance() let btn = wrapper.find('Button') Btn.simulate ('click') // Instance.function () const counts = wrapper.state().time can be used directly in case of inconvenient elements expect(instance.state.isRun).toBeTruthy() expect(instance.state.time).toBe(60) for (let i = 0; i < counts; I++) {jest. RunOnlyPendingTimers ()} expect (instance. State. IsRun). ToBeFalsy () expect (the instance. The state. The time). The place (1) / / simulation WillUnmount Life cycle function wrapper.unmount()})}Copy the code

With wrapper.props(), wrapper.state() is equivalent to instance.props and instance.state

const { props, state, onClickRefresh} = instance
props.style
state.isRun
onClickRefresh()
Copy the code

If instance.onClickRefresh() is used instead of bTN.simulate (‘click’), the BTN click event changes to onClick. Now you have a single test and you need instance.onclick (); If the latter model is used, no changes are required

Use [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro). The functions are more important than the enzymes, so the library has no way to get the props, state, and other implementation details of the component. More on the differences between the two libraries can be found here

In the class component, you can make up for what is missing. For example, you can have the following code to solve the antD form ref missing problem.

instance.formRef = {
        current: {
          resetFields: jest.fn(),
          getFieldValue: jest.fn(),
        },
      }; 
Copy the code

However, there is no problem in the ref instance like this, so it is not good to solve it in this way. It is a kind of stopgap treatment. If there is a better way, I hope you can consult me

Function component

import React, { useEffect, useState } from "react"; import { Select } from "antd"; const { Option } = Select; import { FormSelectItem } from "./FormItem"; import {request} from ".. /utils/request"; import { Observer, useLocalObservable } from "mobx-react"; import store from '.. /lib/store' import { getTranslate } from ".. /utils"; const FormAccountItem = (props) => { const STORE = useLocalObservable(() => store) || {} const [dymaicOptions, setDymaicOptions] = useState([]); useEffect(() => { let mounted = true; async function fetchData() { const params = { pageNo: 1, pageSize: 1000000, }; const { accountList = [] } = await request("accountList", params,STORE); mounted && setDymaicOptions(accountList); } fetchData(); return () => { mounted = false; }; } []); const onValueChange = (value) => { const item = dymaicOptions.find((item) => item.accountId === value); props.onChange(item); }; return ( <FormSelectItem name={"accountId"} label={"Account"} rules={[{ required: true}]} {... props} onChange={onValueChange} > {dymaicOptions.map((item) => { const { accountId, accountName } = item; return ( <Option key={accountId} value={accountId}> {accountName} </Option> ); })} </FormSelectItem> ); }; export default FormAccountItem;Copy the code

Unit testing

import React from "react"; import { mount } from "enzyme"; import FormAccountItem from ".. /src/components/FormAccountItem"; import { request } from ".. /src/utils/request"; import { Form } from "antd"; import { accountResponse } from "./mocks/data"; import { act } from "react-dom/test-utils"; jest.mock(".. /src/utils/request"); const setupFormItem = (component) => { return <Form>{component}</Form>; }; describe(">>>FormAccountItem --- ", () => { it("+++ render ", async () => { await act(async () => { request.mockResolvedValue(accountResponse); const changeFn = jest.fn(); let container = mount( setupFormItem(<FormAccountItem onChange={changeFn} />) ); expect(container.length).toEqual(1); container.unmount(); }); }); });Copy the code

Function components can be used to obtain instance, props, and state when compared with class components in a single test. Therefore, instance.func() can only be used to simulate a function component. However, there are some problems such as antD select component, can not find the Option item, the above onValueChange can not be overridden.

Note:

  1. UseEffect fetchData is directly extracted from testCan't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.Problem, so fetchData encapsulation
  2. Only container.unmount() will run the useEffect cleanup
  3. UseEffect setDymaicOptions are available if not in actWhen testing, code that causes React state updates should be wrapped into act(...) : act(() => { /* fire events that update state */ });
  1. When a component has more than one network request emulation, refer to multiple network request emulation
  1. Verify that setDymaicOptions(setState) is called

    import * as reactModule from "react";
    import { shallow } from "enzyme";
    import MultipleStatesComponent from "./MultipleStatesComponent";
    
    describe("test multiple state in component", () => {
      let wrapper;
      let setDataLength;
      let setLoading;
      let setText;
    
      beforeEach(() => {
        setDataLength = jest.fn(x => {});
        setLoading = jest.fn(x => {});
        setText = jest.fn(x => {});
    
        reactModule.useState = jest
          .fn()
          .mockImplementationOnce(x => [x, setDataLength])
          .mockImplementationOnce(x => [x, setLoading])
          .mockImplementationOnce(x => [x, setText]);
    
        wrapper = shallow(<MultipleStatesComponent />);
      });
    
      it("should test button one", () => {
        wrapper
          .find("button")
          .at(0)
          .simulate("click");
    
        expect(setDataLength).toHaveBeenCalledWith(10);
      });
    Copy the code
  1. Simulation of useEffect

    import * as reactModule from "react";
    import { shallow } from "enzyme";
    import EffectComponent from "./EffectComponent";
    
    describe("test App Component", () => {
      it("should call the logic in useEffect", () => {
        const setDataSize = jest.fn(size => {});
        reactModule.useState = jest.fn(initialDataSize => [
          initialDataSize,
          setDataSize
        ]);
    
        reactModule.useEffect = jest.fn((effectLogic, triggers) => effectLogic());
        wrapper = shallow(<EffectComponent />);
        expect(setDataSize).toHaveBeenCalledWith(3);
      });
    });
    Copy the code

The resources

react function component testing