接续《react-testing-library快速测试组件渲染》,让我们聊聊怎么测试组件的交互行为——即事件响应。
触发事件
在《react-testing-library快速测试组件渲染》中,我介绍了通过@testing-library/react的render()
返回结果中的元素选择器和容器元素快速定位到目标元素。其实除了render()
之外,@testing-library/react还有一个fireEvent
接口帮我们触发事件。
fireEvent
接口在依赖包@testing-library/dom的fireEvent
基础上添加了一些适用于React应用的事件。
它有两种用法。方法一:
fireEvent(node: HTMLElement, event: Event)
这种方式需传入一个Event对象,且不能保证只传入事件回调中用到的参数就可以工作。举个例子:
import * as React from "react";
import { render, fireEvent } from "@testing-library/react";
function MyCompnent() {
const [count, setCount] = React.useState(0);
return <div onClick={() => setCount((count) => count + 1)}>{count}</div>;
}
test("fire click event", () => {
const { container, getByText } = render(<MyCompnent />);
fireEvent(
container.firstChild,
new MouseEvent("click", {
bubbles: true,
})
);
expect(getByText(1)).toBeTruthy();
});
为了测试点击事件,我得用MouseEvent
创建一个事件对象。在测试中,我发现这个事件对象里bubbles
这个属性是必须的,不然就报错。
像这样找到对应的事件类和必要参数有些麻烦,因此提供了第二种方式:
fireEvent[eventName](node: HTMLElement, eventProperties: Object)
这里的eventName
和eventProperties
大大简化了法一的复杂度。具体的参数内容得参照这个代码文件(神马!?竟然懒得写文档直接扔源代码🤯)。
然后上述的测试就可以简化为:
test("fire click event", () => {
const { container, getByText } = render(<MyCompnent />);
fireEvent.click(container.firstChild);
expect(getByText(1)).toBeTruthy();
});
fireEvent
对付简单的时间绰绰有余,然而在碰到稍稍复杂一些的操作如按键(按顺序触发keydown,keypress,keyup)、悬停(触发mouseEnter, mouseOver, mouseMove, mouseOut)就不够用了。
由此testing-library推出了一个@testing-library/user-event库来处理这些常用的操作。
像Click这种简单的与fireEvent
的用法区别不大:
import * as React from "react";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
function MyCompnent() {
const [count, setCount] = React.useState(0);
return <div onClick={() => setCount((count) => count + 1)}>{count}</div>;
}
test("fire click event", () => {
const { container, getByText } = render(<MyCompnent />);
userEvent.click(container.firstChild);
expect(getByText(1)).toBeTruthy();
});
模拟触发一系列事件的操作就简单许多:
import * as React from "react";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
function MyCompnent({ onChange }: { onChange: (e: any) => void }) {
return (
<label>
Value:
<input onChange={onChange} />
</label>
);
}
test("fire change event", () => {
const onChange = jest.fn();
const { getByLabelText } = render(<MyCompnent onChange={onChange} />);
const input = getByLabelText("Value:");
userEvent.type(input, "a");
expect(onChange.mock.calls[0]?.[0]?.target.value).toBe("a");
});
测试响应
事件的响应有多种。包括:
- 调用传入组件的回调函数、其他模块或JS内置接口,可以通过mock测试调用次数和调用的参数;
- 更新组件父级或全局参数,需要用mock防止在异步测试中多个用例之间互相影响;
- 组件DOM有我们期望的变化。参照《react-testing-library快速测试组件渲染》一文。
用mock测试响应
对于用mock测试,我在《用Jest mock隔离单元测试中的不可控因素》一文中有详细介绍对不同情况的处理。以上例子中就涵盖了利用mock测试传入组件回调函数的情况:
test("fire change event", () => {
const onChange = jest.fn();
const { getByLabelText } = render(<MyCompnent onChange={onChange} />);
const input = getByLabelText("Value:");
userEvent.type(input, "a");
expect(onChange.mock.calls[0]?.[0]?.target.value).toBe("a");
});
除了测试了onChange()
调用的参数之外,还可以添加调用次数的测试来保证组件行为与我们期望的相同:
expect(onChange.mock.calls.length).toBe(1);
异步响应
组件内部一般由状态变化或直接DOM操作引发DOM变化。而useState()
钩子和类组件中this.setState()
导致的状态变化是异步的,还可能引发连锁反应变化多次。
由于React内部调度机制和连锁反应的存在让测试程序无法预测什么时候DOM渲染我们期望的变化,我们需要“等待”。
在@testing-library/react中这样的等待可以把getBy
的同步查询替换为findBy
或queryBy
实现:
import * as React from "react";
import { render, waitFor } from "@testing-library/react";
function asyncFunc(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve("b"), 500);
});
}
export default function MyComponent() {
const [state, setState] = React.useState("a");
React.useEffect(() => {
asyncFunc().then(setState);
}, []);
return <div>{state}</div>;
}
test("test render", async () => {
const { getByText, findByText } = render(<MyComponent />);
expect(getByText("a")).toBeTruthy();
expect(await findByText("b")).toBeTruthy();
});
也可以用waitFor()
接口实现:
test("test render", async () => {
const { getByText } = render(<MyComponent />);
expect(getByText("a")).toBeTruthy();
await waitFor(() => expect(getByText("b")).toBeTruthy());
});
看看其他测试系列文:
- 前端写不写单元测试?| 创作者训练营
- React应用测试:配置Typescript, Jest, ESLint | 创作者训练营
- react-testing-library快速测试React组件渲染
- Jest测试CSS module组件报错怎么办?
- 用Jest mock隔离单元测试中的不可控因素