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:
- UseEffect fetchData is directly extracted from test
Can'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 - Only container.unmount() will run the useEffect cleanup
- UseEffect setDymaicOptions are available if not in act
When testing, code that causes React state updates should be wrapped into act(...) : act(() => { /* fire events that update state */ });
- When a component has more than one network request emulation, refer to multiple network request emulation
-
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
-
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