前端单元测试系列:关于 React Testing Library 你“可能”不知道的五件事

作者: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,比如 querySelectorclosest

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 [...]"。

这是什么意思呢?原来匹配器接受字符串、正则表达式或函数。

这个函数会对每个你正在渲染的节点调用。它接收两个参数:节点的内容和节点本身。你所需要做的就是根据节点是否是你想要的,返回 truefalse

一个例子就可以很清楚的说明这一点:

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",要么是一个空字符串。

我们真正检查的是当前节点是否有正确的 textContenthasText 是一个小辅助函数,用于做到这一点。我声明它是为了保持代码整洁。

但这还不是全部。我们的 div 并不是唯一包含我们要查找的文本的节点。例如,在这种情况下,body 也有相同的文本。为了避免返回比需要的更多的节点,我们确保没有子节点有与其父节点相同的文本。这样我们就能确保我们返回的节点是最小的------换句话说,是离 DOM 树底部最近的。

5. 你可以用 user-event 模拟浏览器事件

好吧,这是一个有些自卖自夸的内容,因为我是 user-event 的作者。不过,人们------包括我自己------发现它很有用。也许你也会喜欢。

user-event 所尝试做的一切就是模拟真实用户在与你的应用程序交互时会做的事情。这是什么意思呢?想象一下你有一个 input 字段,在你的测试中,你想要在其中输入一些文本。你可能会这样做:

tsx 复制代码
fireEvent.change(input, { target: { value: "Hello world" } });

它确实可以工作,但它并没有模拟浏览器中发生的事情。真实用户很可能会移动鼠标选择输入字段,然后开始逐个字符地输入。这反过来又会触发许多事件(blurfocusmouseEnterkeyDownkeyUp......)。user-event 会为你模拟所有这些事件:

tsx 复制代码
import userEvent from "@testing-library/user-event";

userEvent.type(input, "Hello world");

查看 README 以了解可用的 API,并随时提出新的建议。


希望这个翻译对你有帮助!

相关推荐
杨凯凡18 小时前
Mockito 全面指南:从单元测试基础到高级模拟技术
java·单元测试·mockito
代码续发19 小时前
如何编写单元测试
单元测试
慵懒学者20 小时前
16 Junit单元测试框架、反射、注解、动态代理(黑马Java视频笔记)
java·笔记·junit·单元测试
测试杂货铺2 天前
白盒测试用例的设计
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·测试用例
杨凯凡2 天前
JUnit 全面指南:从基础到高级测试实践
java·junit·单元测试
zerohawk2 天前
【log4j】配置Slf4j
junit·单元测试·log4j
佟格湾2 天前
单元测试之Arrange-Act-Assert(简称AAA)
单元测试
川石课堂软件测试3 天前
涨薪技术|Docker容器数据管理
运维·功能测试·docker·容器·单元测试
niuniu_6663 天前
Selenium 简单入门操作示例
功能测试·selenium·测试工具·单元测试·dubbo·测试
天航星4 天前
VSCode Java 单元测试没有运行按钮
java·vscode·单元测试