2020 年如何编写 React 测试(第二部分)

2025-06-07

2020 年如何编写 React 测试(第二部分)

使用 React 推荐库编写 React 测试 - Jest 和适用于 React 中级用户的测试库。

请注意

在本文中,我将探讨 React 测试中更高级的概念,希望它们能对你的情况有所帮助。如果你是 React 新手或测试新手,我建议你在继续阅读之前先阅读第一部分,了解一些基础知识,谢谢!

首先,让我们看一下可访问性测试

前端开发是关于可视化和与最终用户的交互,可访问性测试可以确保我们的应用程序能够覆盖尽可能多的用户。

来自 React
来自 - https://reactjs.org/docs/accessibility.html

为应用程序的每个方面编写可访问性测试似乎非常令人生畏,但感谢Deque Systems - 一家致力于通过提供免费在线Axe测试包来提高软件可访问性的公司,我们现在可以通过导入Jest-axe和 Jest Library 来测试 Web 应用程序的可访问性,从而轻松利用来自世界各地的许多高级开发人员的专业知识。



npm install --save-dev jest-axe


Enter fullscreen mode Exit fullscreen mode

或者



yarn add --dev jest-axe


Enter fullscreen mode Exit fullscreen mode

安装软件包后,我们可以将辅助功能测试添加到项目中,如下所示:



// App.test.js
import React from 'react';
import App from './App';
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

describe('App', () => {
  test('should have no accessibility violations', async () => {
    const { container } = render(<App />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});


Enter fullscreen mode Exit fullscreen mode

这将有助于确保您的前端开发符合最新版本的WCAG(Web 内容可访问性指南)。例如,如果您为导航栏组件分配了错误的角色,



// ./components/navBar.js
...
<div className="navbar" role='nav'>
   ...
</div>
...


Enter fullscreen mode Exit fullscreen mode

它会像下面这样提醒你:

A11y 测试错误角色

在此查看 WAI-ARIA 角色列表

将 nav 替换为如下所示的导航角色,测试将通过。



// ./components/navBar.js
...
<div className="navbar" role='navigation'>
   ...
</div>
...


Enter fullscreen mode Exit fullscreen mode

正如我们上面看到的,此测试将有助于确保您遵循WCAG(Web 内容可访问性指南)标准,以便您的应用可以覆盖大多数人。

第二,添加快照测试

当你想确保你的 UI 不会发生意外变化时,快照测试是一个非常有用的工具。——来自Jest

您可以对整个应用程序或某个特定组件进行测试。它们可以在开发周期中发挥不同的作用,您可以使用快照测试来确保应用程序的 UI 不会随时间而变化,或者比较上一个快照与当前输出之间的差异,以迭代开发。

我们以编写整个App的测试为例,展示如何编写快照测试



// App.test.js
import React from 'react';
import App from './App';

import renderer from 'react-test-renderer';
...

describe('App', () => {
  ...

  test('snapShot testing', () => {
    const tree = renderer.create(<App />).toJSON();
    expect(tree).toMatchSnapshot();
  });

});


Enter fullscreen mode Exit fullscreen mode

如果这是第一次运行此测试,Jest 将创建一个快照文件(__snapshots__也会创建一个文件夹“ ”),类似于此。

快照文件树



// App.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`App snapShot testing 1`] = `
<div
  className="App"
>
  <div
    className="navbar"
  >
    ....
```
With this test in place, once you make any change over the DOM, the test will fail and show you exactly what is changed in a prettified format, like the output below:

![snapshot-test-error](https://dev-to-uploads.s3.amazonaws.com/i/etzzsl0hfx3baavp8kvd.png)

In this case, you can either press `u` to update the snapshot or change your code to make the test pass again.

> If you adding a snapshot test at the early stage of development, you might want to turn off the test for a while by adding `x` in front of the test, to avoid getting too many errors and slowing down the process.
```js
 xtest('should have no accessibility violations', async () => {
   ...
  });
```

## Third, let's see how to test a UI with an API call.
It is fairly common now a frontend UI has to fetch some data from an API before it renders its page. Writing tests about it becomes more essential for the Front End development today.

First, let's look at the process and think about how we can test it.

![web-api-datafetch](https://dev-to-uploads.s3.amazonaws.com/i/eh5nxp4ev3m0wfopnd4t.png)

1. When a condition is met (such as click on a button or page loaded), an API call will be triggered;
2. When data come back from API, usually response need to parse before going to next step (optional);
3. When having proper data, the browser starts to render the data accordingly;
4. On the other hand, if something goes wrong, an error message should show up in the browser.

In FrontEnd development, we can test things like below:

* whether the response comes back being correctly parsed?
* whether the data is correctly rendered in the browser in the right place?
* whether the browser show error message when something goes wrong?

However, we should not:
* Test the API call
* Call the real API for testing

> Because most of the time, API is hosted by the third party, the time to fetch data is uncontrollable. Besides, for some APIs, given the same parameters, the data come back may vary, which will make the test result unpredictable.

For testing with an API, we should: 

![web-api-datafetch-Test](https://dev-to-uploads.s3.amazonaws.com/i/iomgaf7qmiuwguu2ze0n.png)

* Use Mock API for testing and return fack data
* Use fake data to compare UI elements to see if they match

***If you got the ideas, let's dive into the real code practice.***

Let's say we want to test the following **News page** component, where it gets the news from `getNews` API call and render them on the browser.
```js
// ./page/News.js
import React, { useState, useEffect } from 'react';
import getNews from '../helpers/getNews';
import NewsTable from '../components/newsTable';

export default () => {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [errorMsg, setErrorMsg] = useState('');
  const subreddit = 'reactjs';

  useEffect(() => {
    getNews(subreddit)
      .then(res => {
        if (res.length > 0) {
          setPosts(res);
        } else {
          throw new Error('No such subreddit!');
        }
      })
      .catch(e => {
        setErrorMsg(e.message);
      })
      .finally(() => {
        setLoading(false);
      });
  }, [])

  return (
    <>
      <h1>What is News Lately?</h1>
      <div>
        {loading && 'Loading news ...'}
        {errorMsg && <p>{errorMsg}</p>}
        {!errorMsg && !loading && <NewsTable news={posts} subreddit={subreddit} />}
      </div>
    </>
  )
}
```
First, let's create a `__mocks__` folder at where the API call file located. (In our case, the API call file call **`getNews.js`**), create the mock API call file with the same name in this folder. Finally, prepare some mock data inside this folder.

![mock API folder](https://dev-to-uploads.s3.amazonaws.com/i/qle8njo9k4h7erxo0f0l.png)

**Mock API** file (`getNews.js`) should look sth like below -
```js
// ./helpers/__mocks__/getNews.js
import mockPosts from './mockPosts_music.json';

// Check if you are using the mock API file, can remove it later
console.log('use mock api'); 

export default () => Promise.resolve(mockPosts);
```
Vs. Real API Call
```js
// ./helpers/getNews.js
import axios from 'axios';
import dayjs from 'dayjs';

// API Reference - https://reddit-api.readthedocs.io/en/latest/#searching-submissions

const BASE_URL = 'https://api.pushshift.io/reddit/submission/search/';

export default async (subreddit) => {
  const threeMonthAgo = dayjs().subtract(3, 'months').unix();
  const numberOfPosts = 5;

  const url = `${BASE_URL}?subreddit=${subreddit}&after=${threeMonthAgo}&size=${numberOfPosts}&sort=desc&sort_type=score`;

  try {
    const response = await axios.get(url);
    if (response.status === 200) {
      return response.data.data.reduce((result, post) => {
        result.push({
          id: post.id,
          title: post.title,
          full_link: post.full_link,
          created_utc: post.created_utc,
          score: post.score,
          num_comments: post.num_comments,
          author: post.author,
        });
        return result;
      }, []);
    }
  } catch (error) {
    throw new Error(error.message);
  }
  return null;
};
```
As we can see from the above codes, a `mock API call` just simply return a resolved mock data, while a `real API call` needs to go online and fetch data every time the test run.

With the mock API and mock data ready, we now start to write tests.
```js
// ./page/News.test.js
import React from 'react';
import { render, screen, act } from '@testing-library/react';
import { BrowserRouter as Router } from "react-router-dom";
import News from './News';

jest.mock('../helpers/getNews');  //adding this line before any test.

// I make this setup function to simplify repeated code later use in tests.
const setup = (component) => (
  render(
   // for react-router working properly in this component
  // if you don't use react-router in your project, you don't need it.
    <Router>
      {component}
    </Router>
  )
);

...
```
> **Please Note:**
```js
jest.mock('../helpers/getNews');
```
> Please add the above code at the beginning of every test file that would possibly trigger the API call, not just the API test file. I make this mistake at the beginning without any notifications, until I add console.log('call real API') to monitor calls during the test.

Next, we start to write a simple test to check whether a title and loading message are shown correctly.
```js
// ./page/News.test.js
...
describe('News Page', () => {
  test('load title and show status', async () => {
    setup(<News />);  //I use setup function to simplify the code.
    screen.getByText('What is News Lately?'); // check if the title show up
    await waitForElementToBeRemoved(() => screen.getByText('Loading news ...'));
  });
...
});
```
![mock_api_first_test_pass](https://dev-to-uploads.s3.amazonaws.com/i/9lznaamxzq3bleuccs3s.png)

With the mock API being called and page rendering as expected. We can now continue to write more complex tests.
```js
...
test('load news from api correctly', async () => {
    setup(<News />);
    screen.getByText('What is News Lately?');

    // wait for API get data back
    await waitForElementToBeRemoved(() => screen.getByText('Loading news ...'));

    screen.getByRole("table");  //check if a table show in UI now
    const rows = screen.getAllByRole("row");  // get all news from the table

    mockNews.forEach((post, index) => {
      const row = rows[index + 1];  // ignore the header row

       // use 'within' limit search range, it is possible have same author for different post
      within(row).getByText(post.title);  // compare row text with mock data 
      within(row).getByText(post.author); 
    })

    expect(getNews).toHaveBeenCalledTimes(1); // I expect the Mock API only been call once
    screen.debug(); // Optionally, you can use debug to print out the whole dom
  });
...
```
> **Please Note**
```js
 expect(getNews).toHaveBeenCalledTimes(1);
```
> This code is essential here to ensure the API call is only called as expected.

When this API call test passes accordingly, we can start to explore something more exciting!

***As we all know, an API call can go wrong sometimes due to various reasons, how are we gonna test it?***

To do that, we need to re-write our mock API file first.
```js
// // ./helpers/__mocks__/getNews.js
console.log('use mock api');  // optionally put here to check if the app calling the Mock API
// check more about mock functions at https://jestjs.io/docs/en/mock-function-api
const getNews = jest.fn().mockResolvedValue([]); 
export default getNews;
```
Then we need to re-write the setup function in `News.test.js` file.

```js
// ./page/News.test.js
...
// need to import mock data and getNews function
import mockNews from '../helpers/__mocks__/mockPosts_music.json';
import getNews from '../helpers/getNews';
...
// now we need to pass state and data to the initial setup
const setup = (component,  state = 'pass', data = mockNews) => {
  if (state === 'pass') {
    getNews.mockResolvedValueOnce(data);
  } else if (state === 'fail') {
    getNews.mockRejectedValue(new Error(data[0]));
  }

  return (
    render(
      <Router>
        {component}
      </Router>
    ))
};
...
```
I pass the default values into the setup function here, so you don't have to change previous tests. But I do suggest pass them in the test instead to make the tests more readable.

Now, let's write the test for API failing.
```js
// ./page/News.test.js
...
test('load news with network errors', async () => {
    // pass whatever error message you want here.
    setup(<News />, 'fail', ['network error']);
    screen.getByText('What is News Lately?');

    await waitForElementToBeRemoved(() => screen.getByText('Loading news ...'));
    screen.getByText('network error');

    expect(getNews).toHaveBeenCalledTimes(1);
  })
...
```
Finally, you can find the complete test code from [here](https://github.com/kelvin8773/react-test-examples/blob/master/src/pages/News.test.js). 

> **Please Note**
They are just simple test cases for demonstration purposes, in the real-world scenarios, the tests would be much more complex. You can check out more testing examples from my other project [here](https://github.com/ooloo-io/reddit-timer-kelvin8773).

---
![Final Lesson](https://dev-to-uploads.s3.amazonaws.com/i/rjst30dgl43v17rpewjq.jpg)
Photo by [ThisisEngineering RAEng](https://unsplash.com/@thisisengineering) on **Unsplash**

## **Final words**
In this article, I followed the best practices **Kent C. Dodds** suggested in his blog post - [Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) published in May 2020, in which you might find my code is slightly different from **[Test-Library Example](https://testing-library.com/docs/react-testing-library/example-intro)** (I think soon Kent will update the docs as well), but I believe that should be how we write the test in 2020 and onward.

I use both **[styled-component](https://testing-library.com/docs/react-testing-library/example-intro)** and in-line style in this project to make UI look better, but it is not necessary, you are free to use whatever CSS framework in react, it should not affect the tests.

Finally, ***Testing*** is an advanced topic in FrontEnd development, I only touch very few aspects of it and I am still learning. If you like me, just starting out, I would suggest you use the examples here or some from [my previous article](https://dev.to/kelvin9877/how-to-write-tests-for-react-in-2020-4oai) to play around with your personal projects. Once you master the fundamentals, you can start to explore more alternatives on the market to find the best fit for your need.

### Here are some resources I recommend to continue learning:
* [Testing from Create React App](https://create-react-app.dev/docs/running-tests)
* [Which Query should I use From Testing Library](https://testing-library.com/docs/guide-which-query)
* [More examples from Testing Library](https://testing-library.com/docs/example-codesandbox)
* [Write Test for Redux from Redux.js](https://redux.js.org/recipes/writing-tests)
* [Unit Test From Gatsby.js](https://www.gatsbyjs.org/docs/unit-testing/)
* [Effective Snapshot Testing](https://kentcdodds.com/blog/effective-snapshot-testing) from [Kent C.Dodds](https://kentcdodds.com/).

### Resources and Article I referenced to finished this article:
* [Inside a dev’s mind — Refactoring and debugging a React test](https://dev.to/jkettmann/inside-a-dev-s-mind-refactoring-and-debugging-a-react-test-2jap) By [Johannes Kettmann](https://dev.to/jkettmann).
* [Don't useEffect as callback!](https://jkettmann.com/dont-useeffect-as-callback/) by [Johannes Kettmann](https://dev.to/jkettmann).
* [Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) by [Kent C.Dodds](https://kentcdodds.com/).
* [Fix the not wrapped act warning](https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning) by [Kent C.Dodds](https://kentcdodds.com/).
* [Accessibility From React](https://reactjs.org/docs/accessibility.html).
* [Axe for Jest](https://www.npmjs.com/package/jest-axe).

### Special Thanks for [Johannes Kettmann](https://dev.to/jkettmann) and his course [ooloo.io](https://ooloo.io/).

> I have learned a lot in the past few months from both the course and fellows from the course - [Martin Kruger](https://github.com/martink-rsa) and [ProxN](https://github.com/ProxN), who help inspire me a lot to finish this testing articles.

> **Below are what I have learned** 
* Creating pixel-perfect designs
* Planning and implementing a complex UI component
* Implement data fetching with error handling
* Debugging inside an IDE
* Writing integration tests
* Professional Git workflow with pull requests
* Code reviews
* Continuous integration

> This is [the Final finishing project](https://github.com/ooloo-io/reddit-timer-kelvin8773) as the outcome.
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/kelvin9877/how-to-write-tests-for-react-in-2020-part-2-26h
PREV
如何在 ExpressJS 中处理密码重置
NEXT
我如何提高工作效率