关于 Testing Library 的一些思考

10 min read

死磕单元测试的过程中,渐渐对 Testing Library 产生了一些思考。正好发现中文网络中对于 Testing Library 的相关内容挺少的 (也可能是我信息茧房了),并且大多数都是 CSDN 之类的货色;于是决定写篇文章好好掰扯掰扯。

Why Testing Library?

Testing Library 是一个流行的轻量级的帮助编写测试的工具箱 (不是框架!!!)。主要是为各种测试框架封装了许多常用的功能 (渲染组件、查找 DOM 节点的方法、各种实用功能)。因为轻量,所以运行速度贼快,这对单元测试至关重要!同时具有很好的通用性,拥有一套放之四海而皆准的最佳实践 (尽可能使测试接近真实的网页使用方式),避免学一个框架学一套测试实践的尴尬;并且拥有强大的功能,可以很轻松的编写出精练又稳健的测试代码。

The more your tests resemble the way your software is used, the more confidence they can give you. 您的测试越像真实软件的使用方式,它们能赋予您的信心就越大。

Testing Library 同时支持多种前端框架与多种测试框架,这里就以博主主修的 React + 最熟悉的 Jest 为例进行讲解。

安装 Testing Library

我们假设你已经安装并配置好了 React + Jest。

然后我们使用 npm 安装 Testing Library:

npm install --save-dev @testing-library/react @testing-library/jest-dom

然后在你的 Jest Setup 文件中引入 Testing Library:

import "@testing-library/jest-dom/extend-expect";

然后就可以愉快的用 Testing Library 来写单元测试了:

import { render, screen } from "@testing-library/react";
import Home from "../pages/index";
import "@testing-library/jest-dom";

describe("Home", () => {
  it("renders a heading", () => {
    const { container } = render(<Home />);

    expect(
      screen.getByRole("heading", {
        name: /react/i,
      }),
    ).toBeInTheDocument();

    expect(container).toMatchSnapshot();
  });
});

使用 Testing Library 编写测试

使用 Testing Library 编写测试大体来讲分为两个步骤:

  1. 渲染组件为 DOM 节点
  2. 查找 DOM 节点并判断其是否符合预期

两个步骤分别对应了我们最常用的两个函数 renderscreen;我们先使用 render 渲染组件,然后在 screen 上查找 DOM 节点,最后用 expect 进行断言。

render 没什么好说的,毕竟不渲染 DOM 树,我们这么测试嘛?

Testing Library 为我们提供了三类方法用于查找 DOM 节点:get(All)By...find(All)By...query(All)By...。它们之间的主要区别在于它们在没找到我们需要的节点时,是抛出错误,还是返回 null,亦或者是返回一个 Promise 并重试。如果我们使用 (get|find|query)By... 进行查询,它们会返回一个准确的 DOM 节点,如果找到了多个符合查找的 DOM 节点则会报错;这种情况我们应该使用 (get|find|query)AllBy...,它们会返回一个 DOM 数组。

查询类型无匹配一个匹配大于一个匹配重试?(Async/Await)
getBy...报错返回 DOM 节点报错No
queryBy...报错返回 DOM 节点报错No
findBy...报错返回 DOM 节点报错Yes
getAllBy...报错返回 DOM 数组返回 DOM 数组No
queryAllBy...返回 []返回 DOM 数组返回 DOM 数组No
findAllBy...报错返回 DOM 数组返回 DOM 数组Yes

更多细节请参考官方文档

按照一般的逻辑,我们一般最常用的是 getByText 这类匹配文字的方法。但实际上 Testing Library 最推荐使用的测试方法是 getByRolegetByRole 查找的是该节点的 WAI-ARIA Roles。我们一般将它称之为可访问性;简单来讲,这是为屏幕阅读器设计的,目的是为了帮助盲人朋友们使用电脑。所以,虽然 getByRole 使用起来比 getByText 更加麻烦,但如果你无法通过 getByRole 查找到你想要访问的节点,那么屏幕阅读器也无法正常的访问到它!换句话说,你网站的可访问性不足!这对普通人来说可能没有什么,但对盲人朋友来说,这是致命的。

另外,WAI-ARIA Roles 这一整套 API 是为屏幕阅读器 (机器) 设计的,而同样作为非自然人的 Testing Library 优先使用这套 API 不是天经地义?

更多关于 WAI-ARIA Roles 和提高网站可访问性的内容请参考:可访问性

还是觉得不知道用什么查询方法?试试 testing-playground

正则表达式

除了精确匹配字符串外,正则表达式也是我们测试的好帮手。

getByTextgetByRole 等常用的查询方法都支持正则表达式。

<form>
  <div>
    <label for="email">Email address</label>
    <input
      type="email"
      aria-describedby="email-help"
      placeholder="Enter email"
    />
  </div>
  <div>
    <label for="password">Password</label>
    <input type="password" placeholder="Password" />
  </div>
  <button type="submit">Submit</button>
</form>
screen.getByText(/email address/i);

screen.getByRole("button", { name: /submit/i });

screen.getByLabelText(/email address/i);

screen.getByPlaceholderText(/enter email/i);

// ...

排除 Loading 阶段对测试的影响

就像我先前提到的,Testing Library 鼓励我们尽可能使测试接近真实的网页使用方式。因此,不到万不得已,Testing Library 并不鼓励我们使用 Mock。

(如何排除网络对测试的影响,答案:MSW)

所以,我们有时会遇到这种情况:组件已经渲染完成,但又没完全完成 (处于 Loading 阶段);但 Testing Library 不知道,这时进行查询,将无法找到目标节点。这时我们需要 find(All)By...waitFor

find(All)By...get(All)By...query(All)By... 最大的区别在于 find(All)By... 会在超时前 (默认 1000ms) 对失败的查询进行重试,直到找到为止。find(All)By... 会返回一个 Promise,因此我们需要使用 await 来获取查询到的节点,好在 Jest 支持异步测试:

describe("PageWithButton", () => {
  it("renders the page with a button", async () => {
    render(<PageWithButton />);

    const button = screen.getByRole("button", { name: "Click Me" });

    fireEvent.click(button);

    await screen.findByText("Clicked once");

    fireEvent.click(button);

    await screen.findByText("Clicked twice");
  });
});

waitFor 的作用和 find(All)By... 类似,它接受一个回调函数作为参数,你可以在回调函数中使用 get(All)By...query(All)By... 查询 DOM 节点并使用 expect 进行断言。在超时前任意查询或者断言失败,waitFor 都会对其进行重试。

describe("PageWithButton", () => {
  it("renders the page with a button", async () => {
    render(<PageWithButton />);

    const button = screen.getByRole("button", { name: "Click Me" });

    fireEvent.click(button);

    await waitFor(() => {
      expect(screen.getByText("Clicked once")).toBeInTheDocument();
      // Other assertions
    });
    // Other test
  });
});

还有一个比较少用的方法 waitForElementToBeRemoved,他的作用是阻塞程序,直到某个 DOM 节点被从 Document 中移除。

describe("WillBeloadedForAWhile", () => {
  it("this page will be loaded for a while", async () => {
    render(<WillBeloadedForAWhile />);

    // The page is loading at this time.

    await waitForElementToBeRemoved(screen.getByText("Loading..."));

    // At this time, the page has been loaded.
  });
});

一些测试时常用的开发工具

screen.debug

在控制台中打印出当前的 DOM Tree。它的本质是对 console.log(prettyDOM()) 的封装。

describe("screen.debug", () => {
  it("Print the current DOM Tree in the console", async () => {
    render(<SomeComponent />);

    screen.debug(); // debug document
    screen.debug(screen.getByText("test")); // debug single element
    screen.debug(screen.getAllByText("multi-test")); // debug multiple elements
  });
});

screen.logTestingPlaygroundURL

在控制台中打印一个链接,点开它就可以在 testing-playground 中交互的调试。

什么是 testing-playground?

describe("screen.debug", () => {
  it("Print the current DOM Tree in the console", async () => {
    render(<SomeComponent />);

    screen.logTestingPlaygroundURL(); // log entire document to testing-playground
    screen.logTestingPlaygroundURL(screen.getByText("test")); // log a single element
  });
});

logRoles

在控制台打印此 DOM 节点和其子节点的所有 WAI-ARIA Roles。需要注意的是 logRoles 不是 screen 的方法 (需要单独导入),并且参数不能为空。

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
</ul>
import { logRoles, render } from "@testing-library/react";

describe("SomeList", () => {
  it("renders some list", async () => {
    const { container } = render(<SomeList />);

    logRoles(container);
  });
});

控制台输出:

navigation:
<nav />
--------------------------------------------------
list:
<ul />
--------------------------------------------------
listitem:
<li />
<li />
--------------------------------------------------

testing-playground

testing-playground 是一个交互式的沙盒 (网页),你可以在其中用鼠标选择 DOM 节点,testing-playground 会告诉你查找次 DOM 节点的最佳查询规则。

testing-playground