官方文档
class 组件
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);
}
}
测试用例
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
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(instance.state.time).toBe(1)
// 模拟 willUnmount生命周期函数
wrapper.unmount()
})
})
通过 wrapper.props(), wrapper.state() 等同于 instance.props 和 instance.state 上述代码可以简写为
const { props, state, onClickRefresh} = instance
props.style
state.isRun
onClickRefresh()
如果是采用了 instance.onClickRefresh() 而不是 btn.simulate('click'), 是有隐患的,btn点击事件变动为 onClick 方法,这时就会出现单测也需要改为 instance.onClick();如果采用后者 simulate 方式就不需要改动
其实 react 官方是推荐使用 [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro) ,和 enzyme 相比是更注重组件的内容(展示)是否正确, 所以这个库没有办法获取组件的props、state等实现细节。更多两个库的区别可以看这里
在class组件里是可以缺啥补啥, 比如可以有以下代码,以便解决 antd form组件的 ref 找不到的问题,
instance.formRef = {
current: {
resetFields: jest.fn(),
getFieldValue: jest.fn(),
},
};
不过类似这种ref实例找不到问题,通过这种方式解决并不是太好,有点头痛医头的意思,有大佬知道更好方法的话,希望能请教下
函数组件
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;
单元测试
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();
});
});
});
函数组件和class组件在单测时相比,函数组件是获取不到instance、props、state的,所以想采用 instance.func() 偷懒的方式是不行的, 只能通过 simulate, 但在有些情况下会遇到一些问题 比如 antd 的select 组件,并不能找到 Option项,上面的 onValueChange 就不能被覆盖到。
注意点:
- useEffect 里的 fetchData 代码直接提取出来,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.问题,所以 fetchData 的封装 - container.unmount() 才会执行 useEffect 的 cleanup
- useEffect里的 setDymaicOptions 如果不在 act 里 会有
When testing, code that causes React state updates should be wrapped into act(...): act(() => { /* fire events that update state */ });
- 当一个组件里有多个网络请求模拟的时候,请参照 多个网络请求返回模拟
-
验证是否setDymaicOptions(setState)被调用
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); });
-
对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); }); });