初学react实现路由跳转_测试React应用的完整初学者指南

闽涵蓄
2023-12-01

初学react实现路由跳转

Building web applications is not an easy task, as of today. To do so, you’re probably using something like React, Vue, or Angular. Your app is faster, the code is both more maintainable and more readable. But that’s not enough. The more your codebase grows, the more complex and buggy it is. So if you care about that, learn to write tests. That’s what we’ll do today for React apps.

到目前为止,构建Web应用程序并非易事。 为此,您可能正在使用React,Vue或Angular之类的工具。 您的应用程序速度更快,代码更易于维护和可读。 但这还不够。 您的代码库增长的越多,它的复杂性和漏洞就越多。 因此,如果您对此有所关注,请学习编写测试。 这就是我们今天要为React应用程序所做的。

Luckily for you, there are already testing solutions for React, especially one: react-testing-library made by Kent C. Dodds. So, let’s discover it, shall we?

幸运的是,已经有针对React的测试解决方案,尤其是其中一种: Kent C. Dodds制作的react-testing-library 。 那么,让我们发现它吧?

为什么要使用React测试库? (Why React Testing Library?)

Basically, React Testing Library (RTL) is made of simple and complete React DOM testing utilities that encourage good testing practices, especially one:

基本上,React测试库(RTL)由简单而完整的React DOM测试实用程序组成,这些实用程序鼓励良好的测试实践,尤其是以下一种:

“The more your tests resemble the way your software is used, the more confidence they can give you. ”— Kent C. Dodds

“测试越类似于软件使用方式,它们给您的信心就越大。 ”- 肯特·多德斯 ( Kent C. Dodds)

In fact, developers tend to test what we call implementation details. Let’s take a simple example to explain it. We want to create a counter that we can both increment and decrement. Here is the implementation (with a class component) with two tests: The first one is written with Enzyme and the other one with React Testing Library.

实际上,开发人员倾向于测试我们所谓的实现细节 。 让我们举一个简单的例子来解释它。 我们想创建一个既可以递增也可以递减的计数器。 这是带有两个测试的实现(带有类组件):第一个是用Enzyme编写的,另一个是用React Testing Library编写的。

import React from "react";
import { shallow } from "enzyme";


import Counter from "./counter";


describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    const wrapper = shallow(<Counter />);
    expect(wrapper.state("count")).toBe(0);


    wrapper.instance().increment();
    expect(wrapper.state("count")).toBe(1);


    wrapper.instance().decrement();
    expect(wrapper.state("count")).toBe(0);
  });
});

Note: Don’t worry if you don’t fully understand the test files because we’ll see all of this afterward.

注意:如果您不完全了解测试文件,请不要担心,因为我们稍后会看到所有这些信息。

Can you guess which test file is the best one and why? If you’re not used to tests, you may think that both are fine. In fact, the two tests make sure that the counter is incremented and decremented. However, the first one is testing implementation details and it has two risks:

您能猜出哪个测试文件是最好的,为什么? 如果您不习惯测试,您可能会认为两者都很好。 实际上,这两个测试可确保计数器递增和递减。 但是,第一个是测试实现细节,它有两个风险:

  • false positive: The test passes even if the code is broken.

    误报:即使代码损坏,测试也会通过。
  • false negative: The test is broken even if the code is right.

    假阴性:即使代码正确,测试也会失败。

假阳性 (False positive)

Let’s say we want to refactor our components because we want to make it possible to set any count value. So we remove our increment and decrement methods and then add a new setCount method. We forgot to wire this new method to our different buttons:

假设我们要重构组件,因为我们希望可以设置任何计数值。 因此,我们删除了incrementdecrement方法,然后添加了一个新的setCount方法。 我们忘记了将此新方法连接到我们的不同按钮:

class Counter extends React.Component {
  // ...
  setCount = (count) => this.setState({ count })
  render() {
    return (
      <div>
        <button onClick={this.decrement}>-</button>
        <p>{this.state.count}</p>
        <button onClick={this.increment}>+</button>
      </div>
    )
  }
}

The first test (Enzyme) will pass, but the second one (RTL) will fail. Indeed, the first one doesn’t care if our buttons are correctly wired to the methods. It just looks at the implementation itself: our increment and decrement method. This is a false positive.

第一个测试(酶)将通过,但第二个测试(RTL)将失败。 确实,第一个并不关心我们的按钮是否正确连接到方法。 它只是看实现本身:我们的incrementdecrement方法。 这是一个误报。

假阴性 (False negative)

Now, what if we wanted to refactor our class component to hooks? We would change its implementation:

现在,如果我们想将类组件重构为钩子呢? 我们将更改其实现:

import React, { useState } from "react"
const Counter = () => {
  const [count, setCount] = useState(0)
  const increment = () => setCount((count) => count + 1)
  const decrement = () => setCount((count) => count - 1)
  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>{count}</p>
      <button onClick={increment}>+</button>
    </div>
  )
}
export default Counter

This time, the first test is going to be broken even if your counter still works. This is a false negative. Enzyme will complain about state not being able to work on functional components:

这次,即使您的计数器仍然可以工作,第一个测试也将被破坏。 这是一个假阴性。 酶会抱怨state无法在功能组件上起作用:

ShallowWrapper::state() can only be called on class components

Then we have to change the test:

然后我们必须更改测试:

import React from "react"
import { shallow } from "enzyme"
import Counter from "./counter"
describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    const setValue = jest.fn()
    const useStateSpy = jest.spyOn(React, "useState")
    useStateSpy.mockImplementation((initialValue) => [initialValue, setValue])
    
    const wrapper = shallow(<Counter />)
    wrapper.find("button").last().props().onClick()
    expect(setValue).toHaveBeenCalledWith(1)
    // We can't make any assumptions here on the real count displayed
    // In fact, the setCount setter is mocked!
    wrapper.find("button").first().props().onClick()
    expect(setValue).toHaveBeenCalledWith(-1)
  })
})

To be honest, I’m not even sure if this is the right way to test it with Enzyme when it comes to hooks. In fact, we can’t even make assumptions about the displayed count because of the mocked setter.

老实说,我什至不确定这是否是在钩子上用酶测试它的正确方法。 实际上,由于设置方法被嘲笑,我们甚至无法对显示的计数做出假设。

However, the test without implementation details works as expected in all cases. So, if we had something to retain so far, it would be to avoid testing implementation details.

但是,没有实现细节的测试在所有情况下都可以正常工作。 因此,如果到目前为止我们要保留一些东西,那就是避免测试实现细节。

Note: I’m not saying Enzyme is bad. I’m just saying testing implementation details will make tests harder to maintain and unreliable. In this article, we are going to use React Testing Library because it encourages testing best practices.

注意:我并不是说酵素不好。 我只是说测试实现细节将使测试难以维护且不可靠。 在本文中,我们将使用React Testing库,因为它鼓励测试最佳实践。

简单的测试步骤 (A Simple Test Step-by-Step)

Maybe there is still an air of mystery around the test written with React Testing Library. As a reminder, here it is:

用React Testing Library编写的测试也许仍然充满神秘感。 提醒一下,这里是:

import React from "react"
import { fireEvent, render, screen } from "@testing-library/react"
import Counter from "./counter"
describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    render(<Counter />)
    const counter = screen.getByText("0")
    const incrementButton = screen.getByText("+")
    const decrementButton = screen.getByText("-")
    fireEvent.click(incrementButton)
    expect(counter.textContent).toEqual("1")
    fireEvent.click(decrementButton)
    expect(counter.textContent).toEqual("0")
  })
})

Let’s decompose it to understand how it’s made. Introducing the AAA pattern: Arrange, Act, Assert.

让我们分解一下以了解其制作方法。 引入AAA模式:安排,执行,声明。

import React from "react"
import { fireEvent, render, screen } from "@testing-library/react"
import Counter from "./counter"
describe("<Counter />", () => {
  it("properly increments and decrements the counter", () => {
    // Arrange
    render(<Counter />)
    const counter = screen.getByText("0")
    const incrementButton = screen.getByText("+")
    const decrementButton = screen.getByText("-")
    // Act
    fireEvent.click(incrementButton)
    // Assert
    expect(counter.textContent).toEqual("1")
    // Act
    fireEvent.click(decrementButton)
    // Assert
    expect(counter.textContent).toEqual("0")
  })
})

Almost all of your tests will be written that way:

几乎所有测试都将以这种方式编写:

  1. You arrange (= set up) your code so that everything is ready for the next steps.

    安排 (=设置)您的代码,以便为下一步做好一切准备。

  2. You act, that is, you perform the steps a user is supposed to do (such as a click).

    您可以执行 ,即执行用户应该执行的步骤(例如单击)。

  3. You make assertions on what is supposed to happen.

    您对应该发生的事情做出断言

安排 (Arrange)

In our test, we’ve done two tasks in the arrange part:

在我们的测试中,我们在安排部分完成了两项任务:

  1. Render the component.

    渲染组件。
  2. Get the different elements of the DOM needed using queries and screen.

    使用查询和screen获得所需的DOM的不同元素。

We can render our component with the render method, which is part of RTL's API, where ui is the component to mount:

我们可以使用render方法渲染组件,该方法是RTL API的一部分,其中ui是要挂载的组件:

function render(
ui: React.ReactElement,
options?: Omit<RenderOptions, 'queries'>
): RenderResult

We can provide some options to render, but they are not often needed, so I'll let you check out what's possible in the docs.

我们可以提供一些render选项,但是并不经常需要它们,因此,我将让您了解docs中可能的功能

Basically, all this function does is render your component using ReactDOM.render (or hydrate, for server-side rendering) in a newly created div appended directly to document.body. You won't often need (at least in the beginning) the result from the render method, so I'll let you check the docs for this as well.

基本上,所有此功能所做的就是在直接附加到document.body的新创建的div使用ReactDOM.render (或为服务器端渲染为ReactDOM.render渲染组件 。 您通常不会(至少在开始时)需要render方法的结果,所以我也将让您检查此文档

Once our component is rendered correctly, we can get the different elements of the DOM using screen queries.

正确呈现组件后,我们可以使用屏幕查询来获取DOM的不同元素。

But what is screen? As said above, the component is rendered in document.body. Since it's common to query it, Testing Library exports an object with every query pre-bound to document.body. Note that we can also destructure queries from the render result, but trust me, it's more convenient to use screen.

但是什么是screen ? 如上所述,该组件在document.body呈现。 由于查询是很常见的,因此测试库会导出一个对象,每个查询都预先绑定到document.body 。 注意,我们也可以 render 结果中解构查询 ,但是请相信我,使用screen更加方便。

And now, you may think, what are these queries? They are utilities that allow you to query the DOM as a user would do it. Thus, you can find elements by label text, by a placeholder, by title.

现在,您可能会想, 这些查询是什么? 它们是实用程序,使您可以像查询用户一样查询DOM。 因此,您可以通过标签文本,占位符,标题来查找元素。

Here are some queries examples taken from the docs:

以下是一些来自docs的查询示例:

  • getByLabelText: searches for the label that matches the given text passed as an argument and then finds the element associated with that label

    getByLabelText :搜索与作为参数传递的给定文本匹配的标签,然后找到与该标签关联的元素

  • getByText: searches for all elements with a text node with textContent matching the given text passed as an argument

    getByText :搜索文本节点与textContent匹配作为参数传递的给定文本的所有元素

  • getByTitle: returns the element with a title attribute matching the given text passed as an argument

    getByTitle :返回具有title属性的元素,该属性与作为参数传递的给定文本匹配

  • getByPlaceholderText: searches for all elements with a placeholder attribute and finds one that matches the given text passed as an argument

    getByPlaceholderText :搜索具有placeholder属性的所有元素,并找到与作为参数传递的给定文本匹配的元素

There are many variants to a particular query:

特定查询有许多变体:

  • getBy: returns the first matching node for a query, throws an error if no elements match or finds more than one match

    getBy :返回查询的第一个匹配节点,如果没有元素匹配或发现多个匹配项,则引发错误

  • getAllBy: returns an array of all matching nodes for a query and throws an error if no elements match

    getAllBy :返回查询的所有匹配节点的数组,如果没有元素匹配则抛出错误

  • queryBy: returns the first matching node for a query and returns null if no elements match. This is useful for asserting an element that is not present.

    queryBy :返回查询的第一个匹配节点,如果没有元素匹配,则返回null。 这对于断言不存在的元素很有用。

  • queryAllBy: returns an array of all matching nodes for a query and returns an empty array ([]) if no elements match

    queryAllBy :返回查询的所有匹配节点的数组,如果没有元素匹配,则返回一个空数组( [] )

  • findBy: returns a promise, which resolves when an element is found which matches the given query

    findBy :返回一个promise,该promise在找到与给定查询匹配的元素时解析

  • findAllBy: returns a promise, which resolves to an array of elements when any elements are found which match the given query

    findAllBy :返回一个promise,当找到与给定查询匹配的任何元素时,它将解析为元素数组

Using the right query at the right time can be challenging. I highly recommend that you check Testing Playground to better know which queries to use in your apps.

在正确的时间使用正确的查询可能具有挑战性。 我强烈建议您选中“ 测试游乐场”以更好地了解在应用程序中使用哪些查询。

Let’s come back to our example:

让我们回到我们的例子:

render(<Counter />)
const counter = screen.getByText("0")
const incrementButton = screen.getByText("+")
const decrementButton = screen.getByText("-")

In this example, we can see that we first render the <Counter/>. The base element of this component will look like the following:

在此示例中,我们可以看到我们首先呈现了<Counter/> 。 该组件的基本元素如下所示:

<body>
<div>
<Counter />
</div>
</body>

Then, thanks to screen.getByText, we can query from document.body the increment button, the decrement button, and the counter. Hence, we will get for each button an instance of HTMLButtonElement and, for the counter, an instance of HTMLParagraphElement.

然后, screen.getByText ,我们可以从document.body查询递增按钮,递减按钮和计数器。 因此,我们将为每个按钮获得一个HTMLButtonElement实例,对于计数器,将获得HTMLParagraphElement实例。

法案 (Act)

Now that everything is set up, we can act. For that, we use fireEvent from DOM Testing Library:

现在一切都准备就绪,我们可以采取行动。 为此,我们使用DOM Testing Library中的 fireEvent

fireEvent((node: HTMLElement), (event: Event))

Simply put, this function takes a DOM node (that you can query with the queries seen above) and fires DOM events such as click, focus, change, etc. There are many other events you can dispatch that you can find by reading DOM Testing Library source code.

简而言之,该函数需要一个DOM节点(您可以使用上面看到的查询进行查询)并触发DOM事件,例如clickfocuschange等。您可以通过阅读DOM Testing来查找许多其他事件。 库源代码

Our example is relatively simple. We just want to click a button, so we simply do:

我们的例子相对简单。 我们只想单击一个按钮,因此只需执行以下操作:

fireEvent.click(incrementButton)
// OR
fireEvent.click(decrementButton)

断言 (Assert)

Here comes the last part. Firing an event usually triggers some changes in your app. We must do some assertions to make sure these changes happened. In our test, a good way to do so is to make sure the count rendered to the user has changed. Thus we just have to assert the textContent property of counter is incremented or decrement:

这是最后一部分。 触发事件通常会触发您的应用程序中的某些更改。 我们必须做一些断言,以确保发生这些变化。 在我们的测试中,这样做的一个好方法是确保呈现给用户的计数已更改。 因此,我们只需要声明countertextContent属性是递增还是递减:

expect(counter.textContent).toEqual("1")
expect(counter.textContent).toEqual("0")

And ta-da! We successfully wrote a test that doesn’t test implementation details.

和塔达! 我们成功编写了一个不测试实现细节的测试。

测试待办事项应用 (Test a To-Do App)

Let’s go deeper into this part by testing a more complex example. The app we’re going to test is a simple to-do app whose features are the following:

让我们通过测试一个更复杂的示例来更深入地研究这一部分。 我们将要测试的应用是一个简单的待办应用,其功能如下:

  • Add a new to-do.

    添加一个新的待办事项。
  • Mark a to-do as completed or active.

    将待办事项标记为已完成或有效。
  • Remove a to-do.

    删除待办事项。
  • Filter the to-dos: all, active, and done to-dos.

    过滤待办事项:所有,活动和已完成的待办事项。

Yes, I know, you may be sick of to-do apps in every tutorial, but hey, they’re great examples!

是的,我知道,您可能会讨厌每个教程中的待办事项应用程序,但嘿,它们是很好的例子!

Here is the code:

这是代码:

import React from "react";


function Todos({ todos: originalTodos }) {
  const filters = ["all", "active", "done"];
  const [input, setInput] = React.useState("");
  const [todos, setTodos] = React.useState(originalTodos || []);
  const [activeFilter, setActiveFilter] = React.useState(filters[0]);


  const addTodo = (e) => {
    if (e.key === "Enter" && input.length > 0) {
      setTodos((todos) => [{ name: input, done: false }, ...todos]);
      setInput("");
    }
  };


  const filteredTodos = React.useMemo(
    () =>
      todos.filter((todo) => {
        if (activeFilter === "all") {
          return todo;
        }


        if (activeFilter === "active") {
          return !todo.done;
        }


        return todo.done;
      }),
    [todos, activeFilter]
  );


  const toggle = (index) => {
    setTodos((todos) =>
      todos.map((todo, i) =>
        index === i ? { ...todo, done: !todo.done } : todo
      )
    );
  };


  const remove = (index) => {
    setTodos((todos) => todos.filter((todo, i) => i !== index));
  };


  return (
    <div>
      <h2 className="title">To-dos</h2>
      <input
        className="input"
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={addTodo}
        value={input}
        placeholder="Add something..."
      />
      <ul className="list-todo">
        {filteredTodos.length > 0 ? (
          filteredTodos.map(({ name, done }, i) => (
            <li key={`${name}-${i}`} className="todo-item">
              <input
                type="checkbox"
                checked={done}
                onChange={() => toggle(i)}
                id={`todo-${i}`}
              />
              <div className="todo-infos">
                <label
                  htmlFor={`todo-${i}`}
                  className={`todo-name ${done ? "todo-name-done" : ""}`}
                >
                  {name}
                </label>
                <button className="todo-delete" onClick={() => remove(i)}>
                  Remove
                </button>
              </div>
            </li>
          ))
        ) : (
          <p className="no-results">No to-dos!</p>
        )}
      </ul>
      <ul className="list-filters">
        {filters.map((filter) => (
          <li
            key={filter}
            className={`filter ${
              activeFilter === filter ? "filter-active" : ""
            }`}
            onClick={() => setActiveFilter(filter)}
          >
            {filter}
          </li>
        ))}
      </ul>
    </div>
  );
}


export default Todos

关于fireEvent的更多信息 (More on fireEvent)

We saw previously how fireEvent allows us to click on a button queried with RTL queries (such as getByText). Let's see how to use other events.

前面我们看到了fireEvent如何允许我们单击RTL查询(如getByText )查询的按钮。 让我们看看如何使用其他事件。

In this app, we can add a new to-do by writing something in the input and pressing the Enter key. We'll need to dispatch two events:

在此应用中,我们可以通过在输入中编写内容并按Enter键来添加新的待办事项。 我们需要调度两个事件:

  • change to add a text in the input

    change以在输入中添加文本

  • keyDown to press the enter key

    keyDown按Enter键

Let’s write the first part of the test:

让我们编写测试的第一部分:

test("adds a new to-do", () => {
  render(<Todos />)
  const input = screen.getByPlaceholderText(/add something/i)
  const todo = "Read Master React Testing"
  screen.getByText("No to-dos!")
  fireEvent.change(input, { target: { value: todo } })
  fireEvent.keyDown(input, { key: "Enter" })
})

In this code, we:

在此代码中,我们:

  1. Query the input by its placeholder.

    通过其占位符查询输入。
  2. Declare the to-do we’re going to add.

    声明我们要添加的待办事项。
  3. Assert there were no to-dos using getByText. (If No to-dos! was not in the app, getByText would throw an error.)

    使用getByText没有待办事项。 (如果应用程序中没有“ No to-dos! ”,则getByText会引发错误。)

  4. Add the to-do in the input.

    在输入中添加待办事项。
  5. Press the enter key.

    按输入键。

One thing that may surprise you is the second argument we pass to fireEvent. Maybe you would expect it to be a single string instead of an object with a target property.

让您惊讶的一件事是我们传递给fireEvent的第二个参数。 也许您希望它是单个字符串,而不是具有target属性的对象。

Well, under the hood, fireEvent dispatches an event to mimic what happens in a real app (it makes use of the dispatchEvent method). Thus, we need to dispatch the event as it would happen in our app, including setting the target property. The same logic goes for the keyDown event and the key property.

好吧,在fireEventfireEvent调度了一个事件来模仿真实应用程序中发生的事情(它利用了dispatchEvent方法)。 因此,我们需要像在应用程序中那样调度事件,包括设置target属性。 keyDown事件和key属性的逻辑相同。

What should happen if we add a new to-do?

如果添加新的待办事项应该怎么办?

  • There should be a new item in the list.

    列表中应该有一个新项目。
  • The input should be empty.

    输入应为空。

Hence, we need to query somehow the new item in the DOM and make sure the value property of the input is empty:

因此,我们需要以某种方式查询DOM中的新项目,并确保输入的value属性为空:

screen.getByText(todo)
expect(input.value).toBe("")

The full test becomes:

完整的测试变为:

test("adds a new to-do", () => {
  render(<Todos />)
  const input = screen.getByPlaceholderText(/add something/i)
  const todo = "Read Master React Testing"
  screen.getByText("No to-dos!")
  fireEvent.change(input, { target: { value: todo } })
  fireEvent.keyDown(input, { key: "Enter" })
  screen.getByText(todo)
  expect(input.value).toBe("")
})

开玩笑更好的断言 (Better assertions with jest-dom)

The more you’ll write tests with RTL, the more you’ll have to write assertions for your different DOM nodes. Writing such assertions can sometimes be repetitive and a bit hard to read. For that, you can install another testing library tool called jest-dom.

使用RTL编写测试的次数越多,则必须为不同的DOM节点编写断言的次数就越多。 编写这样的断言有时可能是重复的,并且有点难以阅读。 为此,您可以安装另一个名为jest-dom的测试库工具。

jest-dom provides a set of custom Jest matchers that you can use to extend Jest. These will make your tests more declarative, clearer to read, and easier to maintain.

jest-dom提供了一组可用于扩展Jest的自定义Jest匹配器。 这些将使您的测试更具声明性,更易于阅读,并且更易于维护。

There are many matchers you can use, such as:

您可以使用许多匹配器,例如:

You can install it with the following command:

您可以使用以下命令进行安装:

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

Then you have to import the package once to extend the Jest matchers:

然后,您必须一次导入软件包以扩展Jest匹配器:

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

Note: I recommend that you do that in src/setupTests.js if you use Create React App. If you don't use CRA, import it in one of the files defined in the setupFilesAfterEnv key of your Jest config.

注意 :如果使用Create React App ,我建议您在src/setupTests.js执行此操作。 如果不使用CRA,则将其导入Jest配置的setupFilesAfterEnv键中定义的文件之一。

Let’s come back to our test. By installing jest-dom, your assertion would become:

让我们回到测试中。 通过安装jest-dom ,您的断言将变为:

expect(input).toHaveValue("")

It’s not much, but it’s more readable, it’s convenient, and it improves the developer experience.

它虽然不多,但更具可读性,便利性并改善了开发人员的体验。

Note: If you want to see more test examples on this to-do app, I created a repo that contains all the examples of this article.

注意: 如果您想在此待办事项应用程序上查看更多测试示例,我创建了一个包含本文所有示例的存储库

异步测试 (Asynchronous Tests)

I agree the counter and the to-do app are contrived examples. In fact, most real-world applications involve asynchronous actions: data fetching, lazy-loaded components, etc. Thus you need to handle them in your tests.

我同意柜台和待办事项应用程式是人为的例子。 实际上,大多数实际应用程序都涉及异步操作:数据获取,延迟加载的组件等。因此,您需要在测试中处理它们。

Luckily for us, RTL gives us asynchronous utilities such as waitForElementToBeRemoved.

对我们来说幸运的是,RTL为我们提供了异步工具,例如waitForElementToBeRemoved

In this part, we will use a straightforward posts app whose features are the following:

在这一部分中,我们将使用一个简单的帖子应用程序,其功能如下:

  • Create a post.

    创建一个帖子。
  • See the newly created post in a list of posts.

    在帖子列表中查看新创建的帖子。
  • See an error if something has gone wrong while creating the post.

    创建帖子时,如果出现问题,请查看错误。

Here is the code:

这是代码:

let nextId = 0


export const addPost = (post) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.1) {
        resolve({ status: 200, data: { ...post, id: nextId++ } })
      } else {
        reject({
          status: 500,
          data: "Something wrong happened. Please, retry.",
        })
      }
    }, 500)
  })
}

Let’s test the post creation feature. To do so, we need to:

让我们测试帖子创建功能。 为此,我们需要:

  1. Mock the API to make sure a post creation doesn’t fail.

    模拟API以确保发布后创建不会失败。
  2. Fill in the tile.

    填写瓷砖。
  3. Fill in the content of the post.

    填写帖子内容。
  4. Click the Post button.

    单击发布按钮。

Let’s first query the corresponding elements:

让我们首先查询相应的元素:

import React from "react"
import { fireEvent, render, screen } from "@testing-library/react"
import { addPost as addPostMock } from "./api"
import Posts from "./Posts"
jest.mock("./api")
describe("Posts", () => {
  test("adds a post", async () => {
    addPostMock.mockImplementation((post) =>
      Promise.resolve({ status: 200, data: { ...post, id: 1 } })
    )
    render(<Posts />)
    const title = screen.getByPlaceholderText(/title/i)
    const content = screen.getByPlaceholderText(/post/i)
    const button = screen.getByText(/post/i)
    const postTitle = "This is a post"
    const postContent = "This is the content of my post"
  })
})

You can see I’ve used queries differently this time. Indeed, when you pass a string to a getBy query, it expects to match exactly that string. If there's something wrong with one character, then the query fails.

您可以看到这次我使用查询的方式有所不同。 确实,当您将字符串传递给getBy查询时,它期望与该字符串完全匹配。 如果一个字符有问题,则查询失败。

However, the queries also accept a regular expression as an argument. It can be handy if you want to quickly query a long text or if you want to query a substring of your sentence in case you’re still not sure of the wording.

但是,查询还接受正则表达式作为参数。 如果您想快速查询长文本或想要查询句子的子字符串,这可能会很方便,以防您仍然不确定措辞。

For example, I know the placeholder of my content should include the word post. But, maybe the placeholder will see its wording change at some point, and I don’t want my tests to break because of this simple change. So I use:

例如,我知道内容的占位符应包含单词post 。 但是,也许占位符会在某个时候看到其措词更改,并且我不希望因为这种简单的更改而导致测试失败。 所以我用:

const content = screen.getByPlaceholderText(/post/i)

Note: For the same reason, I use i to make the search case-insensitive. That way, my test doesn't fail if the case changes. Caution, though! If the wording is important and shouldn't change, don't make use of regular expressions.

注意:出于相同的原因,我使用i来使搜索不区分大小写。 这样,如果情况发生变化,我的测试就不会失败。 但是要小心! 如果措辞很重要且不应更改,请不要使用正则表达式。

Then we have to fire the corresponding events and make sure the post has been added. Let’s try it out:

然后,我们必须触发相应的事件,并确保已添加该帖子。 让我们尝试一下:

test("adds a post", () => {
  // ...
  const postContent = "This is the content of my post"
  fireEvent.change(title, { target: { value: postTitle } })
  fireEvent.change(content, { target: { value: postContent } })
  fireEvent.click(button)
  // Oops, this will fail ❌
  expect(screen.queryByText(postTitle)).toBeInTheDocument()
  expect(screen.queryByText(postContent)).toBeInTheDocument()
})

If we had run this test, it wouldn’t have worked. In fact, RTL can’t query our post title. But why? To answer that question, I’ll have to introduce you to one of your next best friends: debug.

如果我们运行了该测试,它将无法正常工作。 实际上,RTL无法查询我们的帖子标题。 但为什么? 要回答这个问题,我将向您介绍您的下一个最好的朋友: debug

调试测试 (Debugging tests)

Simply put, debug is a utility function attached to the screen object that prints out a representation of your component's associated DOM. Let's use it:

简而言之, debug是一个附加到screen对象的实用程序功能,可以打印出与组件关联的DOM的表示形式。 让我们使用它:

test("adds a post", () => {
  // ...
  fireEvent.change(title, { target: { value: postTitle } })
  fireEvent.change(content, { target: { value: postContent } })
  fireEvent.click(button)
  
  debug()
  
  expect(screen.queryByText(postTitle)).toBeInTheDocument()
  expect(screen.queryByText(postContent)).toBeInTheDocument()
})

In our case, debug outputs something similar to this:

在我们的情况下, debug输出类似于以下内容:

<body>
  <div>
    <div>
      <form class="form">
        <h2>
          Say something
        </h2>
        <input placeholder="Your title" type="text" />
        <textarea placeholder="Your post" rows="5" type="text" /
        <button class="btn" disabled="" type="submit">
          Post ing...
        </button>
      </form>
      <div />
    </div>
  </div>
</body>

Now that we know what your DOM looks like, we can guess what’s happening. The post hasn’t been added. If we closely pay attention, we can see the button’s text is now Posting instead of Post.

既然我们知道您的DOM的外观,我们就可以猜测发生了什么。 该帖子尚未添加。 如果我们密切注意,我们可以看到按钮的文本现在是Posting而不是Post

Do you know why? Because posting a post is asynchronous, and we’re trying to execute the tests without waiting for the asynchronous actions. We’re just in the Loading phase. We can only make sure some stuff is going on:

你知道为什么吗? 因为发布帖子是异步的,所以我们试图在不等待异步操作的情况下执行测试。 我们正处于加载阶段。 我们只能确保发生了一些事情:

test("adds a post", () => {
// ...
fireEvent.click(button) expect(button).toHaveTextContent("Posting")
expect(button).toBeDisabled()
})

等待变更 (Wait for changes)

We can do something about that. More precisely, RTL can do something about that with asynchronous utilities such as waitFor:

我们可以为此做些事情。 更准确地说,RTL可以使用异步工具 (如waitFor来做一些事情:

function waitFor<T>(
  callback: () => void,
  options?: {
    container?: HTMLElement
    timeout?: number
    interval?: number
    onTimeout?: (error: Error) => Error
    mutationObserverOptions?: MutationObserverInit
  }
): Promise<T>

Simply put, waitFor takes a callback which contains expectations and waits for a specific time until these expectations pass.

简而言之, waitFor接受一个包含期望的回调,并等待特定的时间,直到这些期望通过。

By default, this time is at most 1000ms at an interval of 50ms (the first function call is fired immediately). This callback is also run every time a child is added or removed in your component's container using MutationObserver.

默认情况下,此时间最多为1000ms,间隔为50ms(第一个函数调用立即触发)。 每当使用MutationObserver在组件的container添加或删除子项时,也会运行此回调。

We’re going to make use of that function and put our initial assertions in it. The test now becomes:

我们将利用该函数并将初始断言放入其中。 现在测试变为:

import React from "react"
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
// ...
describe("Posts", () => {
  test("adds a post", async () => {
    // ...
    expect(button).toHaveTextContent("Posting")
    expect(button).toBeDisabled()
    await waitFor(() => {
      screen.getByText(postTitle)
      screen.getByText(postContent)
    })
  })
})

If you’re using CRA, maybe you encountered the following error:

如果您使用的是CRA,则可能会遇到以下错误:

TypeError: MutationObserver is not a constructor

That’s normal. DOM Testing Library v7 removed a shim of MutationObserver as it's now widely supported. However, CRA, as the time of writing, still uses an older version of Jest (24 or before) which itself uses a JSDOM environment where MutationObserver doesn't exist.

那很正常 DOM测试库v7删除了MutationObserver的填充程序,因为它现在得到了广泛的支持。 但是,在撰写本文时,CRA仍使用旧版本的Jest(24或更早版本),而Jest本身使用的是不存在MutationObserver的JSDOM环境。

Two steps to fix it. First, install jest-environment-jsdom-sixteen as a dev dependency. Then update your test script in your package.json file:

修复它的两个步骤。 首先,安装jest-environment-jsdom-sixteen作为dev依赖项。 然后在package.json文件中更新test脚本:

"scripts": {
...
"test": "react-scripts test --env=jest-environment-jsdom-sixteen"
...
}

Now it passes.

现在它过去了。

There is also another way of testing asynchronous things with findBy* queries, which is just a combination of getBy* queries and waitFor:

还有另一种使用findBy* 查询测试异步事物的方法,这只是getBy*查询和waitFor的组合:

import React from "react"
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
// ...
describe("Posts", () => {
  test("adds a post", async () => {
    // ...
    expect(button).toHaveTextContent("Posting")
    expect(button).toBeDisabled()
    await screen.findByText(postTitle)
    screen.getByText(postContent)
  })
})

Note: In the past, you could also use waitForElement but it’s deprecated now. Don't worry if you find it in certain tests.

注意:过去,您也可以使用waitForElement但现在已弃用。 如果您在某些测试中找到它,请不要担心。

We know for sure that the API successfully returned the full post after the await statement, so we don't have to put async stuff after.

我们肯定知道API在await语句之后成功返回了完整的帖子,因此我们不必在此之后放置异步内容。

And remember, findByText is asynchronous. If you find yourself forgetting the await statement a little bit too much, I encourage you to install the plugin eslint-plugin-testing-library, which contains a rule that prevents you from doing so.

记住, findByText是异步的。 如果您发现自己忘记了await语句太多,我建议您安装eslint-plugin-testing-library插件 ,该插件包含阻止您这样做的规则

Phew! That part was not easy.

! 那部分并不容易。

Hopefully, these three examples allowed you to have an in-depth look at how you can start to write tests for your React apps, but that’s just the tip of the iceberg. A complex app often makes use of React Router, Redux, React's Context, third-party libraries (react-select, for example). Kent C. Dodds has a complete course on that (and much more) called Testing JavaScript that I really recommend.

希望通过这三个示例,您可以深入了解如何开始为React应用编写测试,但这只是冰山一角。 一个复杂的应用程序经常利用React RouterRedux ,React的Context和第三方库(例如react-select )。 肯特C.多德斯(Kent C. Dodds)在这方面有一本完整的课程(以及更多),我真的建议您称之为测试JavaScript

翻译自: https://medium.com/better-programming/the-complete-beginners-guide-to-testing-react-apps-10964f515bec

初学react实现路由跳转

 类似资料: