使用 Enzyme 和 React Testing 库测试 React Hooks

Avatar of Kingsley Silas
Kingsley Silas

DigitalOcean 为您旅程的每个阶段提供云产品。开始使用 200 美元的免费积分!

当您开始在应用程序中使用 React Hooks 时,您需要确保编写的代码非常可靠。发布有错误的代码可不是什么好事。确保代码无错误的一种方法是编写测试。测试 React Hooks 与一般测试 React 应用程序的方式并没有太大区别。

在本教程中,我们将通过一个使用 Hooks 构建的待办事项应用程序来了解如何做到这一点。我们将介绍如何使用 EzymeReact Testing Library 编写测试,这两个库都可以做到这一点。如果您不熟悉 Enzyme,我们实际上之前发布过一篇关于它的文章,展示了 如何在 React 应用程序中与 Jest 一起使用它。在深入研究测试 React Hooks 之前,检查一下这个文章是个不错的想法。

我们要测试的内容

一个非常标准的待办事项组件看起来像这样

import React, { useState, useRef } from "react";
const Todo = () => {
  const [todos, setTodos] = useState([
    { id: 1, item: "Fix bugs" },
    { id: 2, item: "Take out the trash" }
  ]);
  const todoRef = useRef();
  const removeTodo = id => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  const addTodo = data => {
    let id = todos.length + 1;
    setTodos([
      ...todos,
      {
        id,
        item: data
      }
    ]);
  };
  const handleNewTodo = e => {
    e.preventDefault();
    const item = todoRef.current;
    addTodo(item.value);
    item.value = "";
  };
  return (
    <div className="container">
      <div className="row">
        <div className="col-md-6">
          <h2>Add Todo</h2>
        </div>
      </div>
      <form>
        <div className="row">
          <div className="col-md-6">
            <input
              type="text"
              autoFocus
              ref={todoRef}
              placeholder="Enter a task"
              className="form-control"
              data-testid="input"
            />
          </div>
        </div>
        <div className="row">
          <div className="col-md-6">
            <button
              type="submit"
              onClick={handleNewTodo}
              className="btn btn-primary"
            >
              Add Task
            </button>
          </div>
        </div>
      </form>
      <div className="row todo-list">
        <div className="col-md-6">
          <h3>Lists</h3>
          {!todos.length ? (
            <div className="no-task">No task!</div>
          ) : (
            <ul data-testid="todos">
              {todos.map(todo => {
                return (
                  <li key={todo.id}>
                    <div>
                      <span>{todo.item}</span>
                      <button
                        className="btn btn-danger"
                        data-testid="delete-button"
                        onClick={() => removeTodo(todo.id)}
                      >
                        X
                      </button>
                    </div>
                  </li>
                );
              })}
            </ul>
          )}
        </div>
      </div>
    </div>
  );
};
export default Todo; 

使用 Enzyme 进行测试

在开始测试之前,我们需要安装这些包。是时候启动终端了!

npm install --save-dev enzyme enzyme-adapter-16 

src 目录中,创建一个名为 setupTests.js 的文件。我们将使用它来配置 Enzyme 的适配器。

import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({ adapter: new Adapter() }); 

现在我们可以开始编写测试了!我们想测试四件事

  1. 组件是否渲染
  2. 渲染时是否显示初始待办事项
  3. 我们能否创建一个新的待办事项并获得另外三个待办事项
  4. 我们能否删除其中一个初始待办事项,并只留下一个待办事项

src 目录中,创建一个名为 __tests__ 的文件夹,并在其中创建用于编写 Todo 组件测试的文件。让我们将该文件命名为 Todo.test.js。

完成此操作后,我们可以导入所需的包并创建一个 describe 块,在其中填写我们的测试。

import React from "react";
import { shallow, mount } from "enzyme";
import Todo from "../Todo";

describe("Todo", () => {
  // Tests will go here using `it` blocks
});

测试 1:组件渲染

为此,我们将使用浅渲染。浅渲染允许我们检查组件的 render 方法是否被调用——这就是我们在这里要确认的内容,因为这是我们需要证明组件渲染的证据。

it("renders", () => {
  shallow(<Todo />);
});

测试 2:显示初始待办事项

这里我们将使用 mount 方法,它允许我们比 shallow 提供的更深入。这样,我们可以检查待办事项的长度。

it("displays initial to-dos", () => {
  const wrapper = mount(<Todo />);
  expect(wrapper.find("li")).toHaveLength(2);
});

测试 3:我们可以创建一个新的待办事项并获得另外三个待办事项

让我们考虑一下创建新待办事项的过程

  1. 用户在输入字段中输入值。
  2. 用户点击提交按钮。
  3. 我们总共有三个待办事项,其中第三个是新创建的。
it("adds a new item", () => {
  const wrapper = mount(<Todo />);
  wrapper.find("input").instance().value = "Fix failing test";
  expect(wrapper.find("input").instance().value).toEqual("Fix failing test");
  wrapper.find('[type="submit"]').simulate("click");
  expect(wrapper.find("li")).toHaveLength(3);
  expect(
    wrapper
      .find("li div span")
      .last()
      .text()
  ).toEqual("Fix failing test");
});

我们挂载组件,然后使用 find()instance() 方法设置输入字段的值。在进一步模拟点击事件之前,我们断言输入字段的值设置为“修复失败的测试”,这应该将新项目添加到待办事项列表中。

最后,我们断言列表中有三个项目,并且第三个项目等于我们创建的项目。

测试 4:我们可以删除其中一个初始待办事项,并只留下一个待办事项

it("removes an item", () => {
  const wrapper = mount(<Todo />);
  wrapper
    .find("li button")
    .first()
    .simulate("click");
  expect(wrapper.find("li")).toHaveLength(1);
  expect(wrapper.find("li span").map(item => item.text())).toEqual([
    "Take out the trash"
  ]);
});

在这种情况下,我们通过对第一个项目模拟点击事件来返回待办事项。预期这将调用 removeTodo() 方法,该方法应该删除被点击的项目。然后我们检查我们拥有的项目数量以及返回的项目的价值。

这四个测试的源代码可以在 GitHub 上查看

使用 react-testing-library 进行测试

我们将为此编写三个测试

  1. 初始待办事项是否渲染
  2. 我们能否添加一个新的待办事项
  3. 我们能否删除一个待办事项

让我们首先安装所需的包

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

接下来,我们可以导入包和文件

import React from "react";
import { render, fireEvent } from "@testing-library/react";
import Todo from "../Todo";
import "@testing-library/jest-dom/extend-expect";

test("Todo", () => {
  // Tests go here
}

测试 1:渲染初始待办事项

我们将在 test 块中编写我们的测试。第一个测试将如下所示

it("displays initial to-dos", () => {
  const { getByTestId } = render(<Todo />);
  const todos = getByTestId("todos");
  expect(todos.children.length).toBe(2);
});

这里发生了什么?我们使用 getTestId 返回 data-testid 与传递给方法的 data-testid 匹配的元素的节点。在本例中,是 <ul> 元素。然后,我们检查它是否总共有两个子元素(每个子元素都是无序列表内的 <li> 元素)。由于初始待办事项等于两个,因此这将通过。

测试 2:我们可以添加一个新的待办事项

我们也在这里使用 getTestById 返回与我们传入的参数匹配的节点。

it("adds a new to-do", () => {
  const { getByTestId, getByText } = render(<Todo />);
  const input = getByTestId("input");
  const todos = getByTestId("todos");
  input.value = "Fix failing tests";
  fireEvent.click(getByText("Add Task"));
  expect(todos.children.length).toBe(3);
});

我们使用 getByTestId 返回输入字段和 ul 元素,就像我们之前做的那样。为了模拟添加新待办事项的点击事件,我们使用 fireEvent.click() 并传入 getByText() 方法,该方法返回文本与我们传递的参数匹配的节点。从那里,我们可以通过检查子元素数组的长度来检查待办事项的长度。

测试 3:我们可以删除一个待办事项

这将有点类似于我们之前做的事情

it("deletes a to-do", () => {
  const { getAllByTestId, getByTestId } = render(<Todo />);
  const todos = getByTestId("todos");
  const deleteButton = getAllByTestId("delete-button");
  const first = deleteButton[0];
  fireEvent.click(first);
  expect(todos.children.length).toBe(1);
});

我们使用 getAllByTestId 返回删除按钮的节点。由于我们只想删除一个项目,因此我们在集合中的第一个项目上触发点击事件,这应该会删除第一个待办事项。然后这将使 todos 子元素的长度等于 1。

这些测试也可以在 GitHub 上找到

代码风格检查

在使用 Hooks 时,需要遵守两条代码风格检查规则

规则 1:在顶层调用 Hook

…而不是在条件语句、循环或嵌套函数内部。

// Don't do this!
if (Math.random() > 0.5) {
  const [invalid, updateInvalid] = useState(false);
}

这违反了第一条规则。根据官方文档,React 依赖于 Hook 的调用顺序来关联状态和相应的useState调用。此代码破坏了顺序,因为只有在条件为真时才会调用 Hook。

这也适用于useEffect和其他 Hook。查看文档以获取更多详细信息。

规则 2:从 React 函数组件中调用 Hook

Hook 旨在用于 React 函数组件中,而不是 React 的类组件或 JavaScript 函数中。

我们基本上已经介绍了在代码检查方面不应该做什么。我们可以使用一个npm 包来避免这些错误,该包专门用于执行这些规则。

npm install eslint-plugin-react-hooks --save-dev

以下是如何添加到包的配置文件中以使其生效

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

如果您正在使用Create React App,那么您应该知道该包从v3.0.0版本开始就支持 lint 插件。

开始编写可靠的 React 代码吧!

React Hook 与应用程序中的任何其他内容一样,也容易出错,您需要确保正确使用它们。正如我们刚刚看到的,我们可以通过几种方法来实现这一点。您可以使用EnzymeReact Testing Library来编写测试,这完全取决于您。无论哪种方式,请尝试在编写代码时使用代码检查,毫无疑问,您会庆幸自己这么做了。