作者:polvara.me
原文:Five Things You (Probably) Didn't Know About Testing Library
译者:legend80s@
JavaScript与编程艺术
🤝 AI大家还可以结合我的这篇文章高级前端程序员的 10 个实战技巧 ⚔️一起看
本文内容是我通常看到初学者会感到困惑的概念。同时,学习这些概念会极大地提升你的测试水平。当然它对我自己的测试技能提升有很大帮助。
1. 一切都是 DOM 节点
这是初学者在开始接触 Testing Library 时通常会有的第一个误解。尤其是像我这样从 Enzyme 转过来的开发者。
许多人认为 getByText
和其他辅助方法返回的是围绕 React 组件的某种特殊包装。人们甚至会问如何实现一个 getByReactComponent
辅助方法。
当你使用 Testing Library 时,你处理的是 DOM 节点。 这一点在第一条指导原则中已经明确:
如果它与渲染组件有关,那么它应该处理 DOM 节点,而不是组件实例,并且不应鼓励处理组件实例。
如果你想亲自验证一下,这很简单:
tsx
import React from "react";
import { render, screen } from "@testing-library/react";
test("everything is a node", () => {
const Foo = () => <div>Hello</div>;
render(<Foo />);
expect(screen.getByText("Hello")).toBeInstanceOf(Node);
});
一旦你意识到你处理的是 DOM 节点,你就可以开始利用所有 DOM API,比如 querySelector
或 closest
:
tsx
import React from "react";
import { render, screen } from "@testing-library/react";
test("the button has type of reset", () => {
const ResetButton = () => (
<button type="reset">
<div>Reset</div>
</button>
);
render(<ResetButton />);
const node = screen.getByText("Reset");
// 这不会奏效,因为 `node` 是 `<div>` 这时使用 `closest` 非常合适
// expect(node).toHaveProperty("type", "reset");
expect(node.closest("button")).toHaveProperty("type", "reset");
});
2. debug
的可选参数
既然我们现在知道我们处理的是 DOM 结构,那么能够"看到"它会很有帮助。这就是 debug
的用途:
tsx
render(<MyComponent />);
screen.debug();
有时 debug
的输出可能非常长且难以浏览。在这种情况下,你可能希望隔离整个结构的一个子树 。你可以通过将一个节点传递给 debug
来轻松做到这一点:
tsx
render(<MyComponent />);
const button = screen.getByText("Click me").closest();
screen.debug(button);
3. 使用 within
限制查询范围
想象一下,你正在测试一个渲染出以下结构的组件:
html
<table>
<thead>
<tr>
<th>ID</th>
<th>Fruit</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Apples</td>
</tr>
<tr>
<td>2</td>
<td>Oranges</td>
</tr>
<tr>
<td>3</td>
<td>Apples</td>
</tr>
</tbody>
</table>
你想要测试每个 ID 是否得到了正确的值。你不能使用 getByText('Apples')
,因为有两个节点的值是"Apples"。即使没有这种情况,你也不能保证文本位于正确的行中。
其实我们只需要在当前考虑的行内运行 getByText
。这正是 within
的用途:
tsx
import React from "react";
import { render, screen, within } from "@testing-library/react"; // 高亮行
import "jest-dom/extend-expect";
test("the values are in the table", () => {
const MyTable = ({ values }) => (
<table>
<thead>
<tr>
<th>ID</th>
<th>Fruits</th>
</tr>
</thead>
<tbody>
{values.map(([id, fruit]) => (
<tr key={id}>
<td>{id}</td>
<td>{fruit}</td>
</tr>
))}
</tbody>
</table>
);
const values = [
["1", "Apples"],
["2", "Oranges"],
["3", "Apples"],
];
render(<MyTable values={values} />);
values.forEach(([id, fruit]) => {
const row = screen.getByText(id).closest("tr");
// 高亮开始
const utils = within(row);
expect(utils.getByText(id)).toBeInTheDocument();
expect(utils.getByText(fruit)).toBeInTheDocument();
// 高亮结束
});
});
4. 查询也接受函数
你可能见过这样的错误:
Unable to find an element with the text: Hello world. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
通常,这是因为你的 HTML 看起来像这样:
tsx
<div>Hello <span>world</span></div>
解决方案就在错误消息中:"[...] you can provide a function for your text matcher [...]"。
这是什么意思呢?原来匹配器接受字符串、正则表达式或函数。
这个函数会对每个你正在渲染的节点调用。它接收两个参数:节点的内容和节点本身。你所需要做的就是根据节点是否是你想要的,返回 true
或 false
。
一个例子就可以很清楚的说明这一点:
tsx
import { render, screen, within } from "@testing-library/react";
import "jest-dom/extend-expect";
test("pass functions to matchers", () => {
const Hello = () => (
<div>
Hello <span>world</span>
</div>
);
render(<Hello />);
// 这些不会匹配
// getByText("Hello world");
// getByText(/Hello world/);
screen.getByText((content, node) => {
const hasText = (node) => node.textContent === "Hello world";
const nodeHasText = hasText(node);
const childrenDontHaveText = Array.from(node.children).every(
(child) => !hasText(child)
);
return nodeHasText && childrenDontHaveText;
});
});
我们忽略 content
参数,因为在这种情况下,它要么是"Hello",要么是"world",要么是一个空字符串。
我们真正检查的是当前节点是否有正确的 textContent
。hasText
是一个小辅助函数,用于做到这一点。我声明它是为了保持代码整洁。
但这还不是全部。我们的 div
并不是唯一包含我们要查找的文本的节点。例如,在这种情况下,body
也有相同的文本。为了避免返回比需要的更多的节点,我们确保没有子节点有与其父节点相同的文本。这样我们就能确保我们返回的节点是最小的------换句话说,是离 DOM 树底部最近的。
5. 你可以用 user-event
模拟浏览器事件
好吧,这是一个有些自卖自夸的内容,因为我是 user-event
的作者。不过,人们------包括我自己------发现它很有用。也许你也会喜欢。
user-event
所尝试做的一切就是模拟真实用户在与你的应用程序交互时会做的事情。这是什么意思呢?想象一下你有一个 input
字段,在你的测试中,你想要在其中输入一些文本。你可能会这样做:
tsx
fireEvent.change(input, { target: { value: "Hello world" } });
它确实可以工作,但它并没有模拟浏览器中发生的事情。真实用户很可能会移动鼠标选择输入字段,然后开始逐个字符地输入。这反过来又会触发许多事件(blur
、focus
、mouseEnter
、keyDown
、keyUp
......)。user-event
会为你模拟所有这些事件:
tsx
import userEvent from "@testing-library/user-event";
userEvent.type(input, "Hello world");
查看 README 以了解可用的 API,并随时提出新的建议。
希望这个翻译对你有帮助!