如何使用React Testing Library和Jest测试React应用

原文连接:How to Start Testing Your React Apps Using the React Testing Library and Jestjavascript

写测试一般都会被认做一个乏味的过程,可是这是你必须掌握的一个技能,虽然在某些时候,测试并非必要的。而后对于大多数有追求的公司而言,单元测试是必须的,开发者对于代码的自信会大幅提升,侧面来讲也能提升公司对其产品的信心,也能让用户使用得更安心。html

在 React 世界中,咱们使用 react-testing-libraryjest 配合使用来测试咱们的 React Apps。前端

在本文中,我将向你介绍如何使用 8 种简单的方式来来测试你的 React App。java

先备条件

本教程假定你对 React 有必定程度的了解,本教程只会专一于单元测试。react

接下来,在终端中运行如下命令来克隆已经集成了必要插件的项目:ios

git clone https://github.com/ibrahima92/prep-react-testing-library-guide
复制代码

安装依赖:git

npm install
复制代码

或者使用 Yarn :github

yarn
复制代码

好了,就这些,如今让咱们了解一些基础知识!npm

基础知识

本文将大量使用一些关键内容,了解它们的做用能够帮助你快速理解。redux

it 或 test :用于描述测试自己,其包含两个参数,第一个是该测试的描述,第二个是执行测试的函数。

expect :表示测试须要经过的条件,它将接收到的参数与 matcher  进行比较。

matcher :一个但愿到达预期条件的函数,称其为匹配器。

render :用于渲染给定组件的方法。

import React from "react";
import { render } from "@testing-library/react";
import App from "./App";

it("should take a snapshot", () => {
  const { asFragment } = render(<App />);

  expect(asFragment(<App />)).toMatchSnapshot();
});
复制代码

如上所示,咱们使用 it  来描述一个测试,而后使用 render  方法来显示 App 这个组件,同时还期待的是 asFragment(<App />)  的结果与 toMatchSnapshot()  这个 matcher  匹配(由 jest 提供的匹配器)。

顺便说一句, render  方法返回了几种咱们能够用来测试功能的方法,咱们还使用了对象解构来获取到某个方法。

那么,让咱们继续并在下一节中进一步了解 React Testing Library 吧~ 。

什么是 React Testing Library ?

React Testing Library 是用于测试 React 组件的很是便捷的解决方案。 它在 react-dom  和 react-dom/test-utils  之上提供了轻量且实用的 API,若是你打开 React 官网中的测试工具推荐,你会发现 Note 中写了:

注意: 咱们推荐使用 React Testing Library,它使得针对组件编写测试用例就像终端用户在使用它同样方便。

React Testing Library 是一个 DOM 测试库,这意味着它并不会直接处理渲染的 React 组件实例,而是处理 DOM 元素以及它们在实际用户面前的行为。

这是一个很棒的库,(相对)易于使用,而且鼓励良好的测试实践。 固然,你也能够在没有 Jest 的状况下使用它。

“你的测试与软件的使用方式越接近,就能越给你信心。”

那么,让咱们在下一部分中就开始使用它吧。顺便说一下,你不须要安装任何依赖了,刚才克隆的项目自己是用 create-react-app  建立的,已经集成了编写单元测试所须要的插件了,只需保证你已经安装了依赖便可。

8 个示例

1.如何建立测试快照

顾名思义,快照使咱们能够保存给定组件的快照。 当你对组件进行一些更新或重构,但愿获取或比较更改时,它会颇有帮助。

如今,让咱们对 App.js  文件进行快照测试。

  • App.test.js
import React from "react";
import { render, cleanup } from "@testing-library/react";
import App from "./App";

afterEach(cleanup);

it("should take a snapshot", () => {
  const { asFragment } = render(<App />);

  expect(asFragment(<App />)).toMatchSnapshot();
});
复制代码

要得到快照,咱们首先须要导入 render  和 cleanup  方法。 在本文中,咱们将常用这两种方法。

你大概也猜到了, render  方法用于渲染 React 组件, cleanup  方法将做为参数传递给 afterEach ,目的是在每一个测试完成后清除全部内容,以免内存泄漏。

接下来,咱们可使用 render  渲染 App 组件,并从该方法返回 asFragment 。 最后,确保 App 组件的片断与快照匹配。

如今,要运行测试,请打开终端并导航到项目的根目录,而后运行如下命令:

yarn test
复制代码

若是你使用 NPM:

npm run test
复制代码

结果,它将在 src  中建立一个新文件夹 __snapshots__  和及其目录下新建一个 App.test.js.snap  文件,以下所示:

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

exports[`should take a snapshot 1`] = ` <DocumentFragment> <div class="App"> <h1> Testing Updated </h1> </div> </DocumentFragment> `;
复制代码

若是如今你对 App.js  进行更改,则测试将失败,由于快照将再也不符合条件。要使其经过,只需按键盘上的 u  健便可对其进行更新。 而且你将在 App.test.js.snap  中拥有更新后的快照。

如今,让咱们继续并开始测试咱们的元素。

2.测试 DOM 元素

为了测试咱们的 DOM 元素,咱们先大概看下 components/TestElements.js  文件。

  • TestElements.js :
import React from "react";

const TestElements = () => {
  const [counter, setCounter] = React.useState(0);

  return (
    <> <h1 data-testid="counter">{counter}</h1> <button data-testid="button-up" onClick={() => setCounter(counter + 1)}>Up</button> <button disabled data-testid="button-down" onClick={() => setCounter(counter - 1)} > Down </button> </>
  );
};

export default TestElements;
复制代码

你惟一须要留意的就是 data-testid 。 它将用于从测试文件中获取到这些 dom 元素。 如今,让咱们编写单元测试:

测试计数器(counter)是否等于 0

  • TestElements.test.js :
import React from "react";
import { render, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestElements from "./TestElements";

afterEach(cleanup);

it("should equal to 0", () => {
  const { getByTestId } = render(<TestElements />);
  expect(getByTestId("counter")).toHaveTextContent(0);
});
复制代码

如你所见,语法其实和先前的快照测试很是类似。惟一的区别是,咱们如今使用 getByTestId  进行 dom 元素的获取,而后检查该元素的文本内容是否为 0 。

测试 button 按钮是禁用仍是启用

  • TestElements.test.js (将如下代码追加到该文件中):
it("should be enabled", () => {
  const { getByTestId } = render(<TestElements />);
  expect(getByTestId("button-up")).not.toHaveAttribute("disabled");
});

it("should be disabled", () => {
  const { getByTestId } = render(<TestElements />);
  expect(getByTestId("button-down")).toBeDisabled();
});
复制代码

一样地,咱们使用 getByTestId  来获取 dom 元素,第一个测试是测试 button 元素上没有属性 disabled ;第二个测试是测试 button 元素处于禁用状态。

保存以后再运行测试命令,你会发现测试所有经过了!

恭喜你成功经过了本身的第一个测试!

如今,让咱们在下一部分中学习如何测试事件。

3.测试事件

在写单元测试以前,咱们先来看看 components/TestEvents.js  文件是啥样:

  • TestEvents.js :
import React from "react";

const TestEvents = () => {
  const [counter, setCounter] = React.useState(0);

  return (
    <> <h1 data-testid="counter">{counter}</h1> <button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up </button> <button data-testid="button-down" onClick={() => setCounter(counter - 1)}> Down </button> </>
  );
};

export default TestEvents;
复制代码

如今,让咱们为这个组件写单元测试。

单击按钮时,测试计数器是否正确递增和递减

  • TestEvents.test.js :
import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestEvents from "./TestEvents";

afterEach(cleanup);

it("increments counter", () => {
  const { getByTestId } = render(<TestEvents />);

  fireEvent.click(getByTestId("button-up"));

  expect(getByTestId("counter")).toHaveTextContent("1");
});

it("decrements counter", () => {
  const { getByTestId } = render(<TestEvents />);

  fireEvent.click(getByTestId("button-down"));

  expect(getByTestId("counter")).toHaveTextContent("-1");
});
复制代码

如你所见,除了预期的文本内容不一样以外,这两个测试很是类似。

第一个测试使用 fireEvent.click()  触发 click 事件,以检查单击按钮时计数器是否增长为 1 。

第二个测试检查单击按钮时计数器是否递减到 -1 。

fireEvent  有几种可用于测试事件的方法,所以请随时阅读文档以了解更多信息。

如今咱们知道了如何测试事件,让咱们继续学习下一节如何处理异步操做。

4.测试异步操做

异步操做须要花费一些时间才能完成。它能够是 HTTP 请求,计时器等。

一样地,让咱们检查一下 components/TestAsync.js  文件。

  • TestAsync.js :
import React from "react";

const TestAsync = () => {
  const [counter, setCounter] = React.useState(0);

  const delayCount = () =>
    setTimeout(() => {
      setCounter(counter + 1);
    }, 500);

  return (
    <> <h1 data-testid="counter">{counter}</h1> <button data-testid="button-up" onClick={delayCount}> Up </button> <button data-testid="button-down" onClick={() => setCounter(counter - 1)}> Down </button> </>
  );
};

export default TestAsync;
复制代码

在这里,咱们使用 setTimeout()  模拟异步。

测试计数器是否在 0.5s 后递增

  • TestAsync.test.js :
import React from "react";
import {
  render,
  cleanup,
  fireEvent,
  waitForElement,
} from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestAsync from "./TestAsync";

afterEach(cleanup);

it("increments counter after 0.5s", async () => {
  const { getByTestId, getByText } = render(<TestAsync />);

  fireEvent.click(getByTestId("button-up"));

  const counter = await waitForElement(() => getByText("1"));

  expect(counter).toHaveTextContent("1");
});
复制代码

为了测试递增事件,咱们首先必须使用 async/await  来处理该动做,由于正如我以前所说的,它须要一段时间以后才能完成。

随着咱们使用了一个新的辅助方法 getByText() ,这与 getByTestId()  类似,只是如今咱们经过 dom 元素的文本内容去获取该元素而已,而不是以前使用的 test-id 。

如今,单击按钮后,咱们等待使用 waitForElement(() => getByText('1'))  递增计数器。 计数器增长到 1  后,咱们如今能够移至条件并检查计数器是否有效等于 1 。

是否是理解起来很简单?话虽如此,让咱们如今转到更复杂的测试用例。

你准备好了吗?

5.测试 React Redux

若是您不熟悉 React Redux,本文可能会为你提供些许帮助。先让咱们看一下 components/TestRedux.js  的内容。

  • TestRedux.js :
import React from "react";
import { connect } from "react-redux";

const TestRedux = ({ counter, dispatch }) => {
  const increment = () => dispatch({ type: "INCREMENT" });
  const decrement = () => dispatch({ type: "DECREMENT" });

  return (
    <> <h1 data-testid="counter">{counter}</h1> <button data-testid="button-up" onClick={increment}> Up </button> <button data-testid="button-down" onClick={decrement}> Down </button> </>
  );
};

export default connect((state) => ({ counter: state.count }))(TestRedux);
复制代码

再看看 store/reducer.js :

export const initialState = {
  count: 0,
};

export function reducer(state = initialState, action) {
  switch (action.type) {
    case "INCREMENT":
      return {
        count: state.count + 1,
      };
    case "DECREMENT":
      return {
        count: state.count - 1,
      };
    default:
      return state;
  }
}
复制代码

如你所见,没有什么花哨的东西 - 它只是由 React Redux 处理的基本计数器组件。

如今,让咱们编写单元测试。

测试初始状态是否等于 0

  • TestRedux.test.js :
import React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { initialState, reducer } from "../store/reducer";
import TestRedux from "./TestRedux";

const renderWithRedux = ( component, { initialState, store = createStore(reducer, initialState) } = {} ) => {
  return {
    ...render(<Provider store={store}>{component}</Provider>),
    store,
  };
};

afterEach(cleanup);

it("checks initial state is equal to 0", () => {
  const { getByTestId } = renderWithRedux(<TestRedux />);
  expect(getByTestId("counter")).toHaveTextContent("0");
});
复制代码

咱们须要导入一些内容来测试 React Redux。在这里,咱们建立了本身的辅助函数 renderWithRedux()  来渲染组件,由于它将屡次被使用到。

renderWithRedux()  接收要渲染的组件, initialState  和 store  做为参数。若是没有 store ,它将建立一个新 store ,若是没有收到 initialState  或 store ,则将返回一个空对象。

接下来,咱们使用 render()  渲染组件并将 store  传递给 Provider 。

意味着,咱们如今能够将组件 TestRedux  传递给 renderWithRedux()  来测试计数器是否等于 0 。

测试计数器是否正确递增和递减

  • TestRedux.test.js (将如下代码追加到该文件中):
it("increments the counter through redux", () => {
  const { getByTestId } = renderWithRedux(<TestRedux />, {
    initialState: { count: 5 },
  });
  fireEvent.click(getByTestId("button-up"));
  expect(getByTestId("counter")).toHaveTextContent("6");
});

it("decrements the counter through redux", () => {
  const { getByTestId } = renderWithRedux(<TestRedux />, {
    initialState: { count: 100 },
  });
  fireEvent.click(getByTestId("button-down"));
  expect(getByTestId("counter")).toHaveTextContent("99");
});
复制代码

为了测试递增和递减事件,咱们将initialState 做为第二个参数传递给 renderWithRedux() 。 如今,咱们能够单击按钮并测试预期结果是否符合条件。

如今,让咱们进入下一部分并介绍 React Context。

再接下来是 React Router 和 Axios,你还会看下去吗?

6.测试 React Context

若是您不熟悉 React Context,请先阅读本文。另外,让咱们看下 components/TextContext.js  文件。

  • TextContext.js :
import React, { createContext, useContext, useState } from "react";

export const CounterContext = createContext();

const CounterProvider = () => {
  const [counter, setCounter] = useState(0);
  const increment = () => setCounter(counter + 1);
  const decrement = () => setCounter(counter - 1);

  return (
    <CounterContext.Provider value={{ counter, increment, decrement }}> <Counter /> </CounterContext.Provider>
  );
};

export const Counter = () => {
  const { counter, increment, decrement } = useContext(CounterContext);
  return (
    <> <h1 data-testid="counter">{counter}</h1> <button data-testid="button-up" onClick={increment}> Up </button> <button data-testid="button-down" onClick={decrement}> Down </button> </>
  );
};

export default CounterProvider;
复制代码

如今计数器状态经过 React Context 进行管理,让咱们编写单元测试以检查其行为是否符合预期。

测试初始状态是否等于 0

  • TestContext.test.js :
import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import CounterProvider, { CounterContext, Counter } from "./TestContext";

const renderWithContext = (component) => {
  return {
    ...render(
      <CounterProvider value={CounterContext}>{component}</CounterProvider>
    ),
  };
};

afterEach(cleanup);

it("checks if initial state is equal to 0", () => {
  const { getByTestId } = renderWithContext(<Counter />);
  expect(getByTestId("counter")).toHaveTextContent("0");
});
复制代码

与上一节关于 React Redux 的部分同样,这里咱们经过建立一个辅助函数 renderWithContext()  来渲染组件。可是此次,它仅接收组件做为参数。 为了建立一个新的上下文,咱们将 CounterContext  传递给 Provider。

如今,咱们就能够测试计数器初始状态是否等于 0 。

测试计数器是否正确递增和递减

  • TestContext.test.js (将如下代码追加到该文件中):
it("increments the counter", () => {
  const { getByTestId } = renderWithContext(<Counter />);

  fireEvent.click(getByTestId("button-up"));
  expect(getByTestId("counter")).toHaveTextContent("1");
});

it("decrements the counter", () => {
  const { getByTestId } = renderWithContext(<Counter />);

  fireEvent.click(getByTestId("button-down"));
  expect(getByTestId("counter")).toHaveTextContent("-1");
});
复制代码

如你所见,这里咱们触发一个 click 事件,测试计数器是否正确地增长到 1  或减小到 -1 。

咱们如今能够进入下一节并介绍 React Router。

7.测试 React Router

若是您想深刻研究 React Router,这篇文章可能会对你有所帮助。如今,让咱们先 components/TestRouter.js  文件。

  • TestRouter.js :
import React from "react";
import { Link, Route, Switch, useParams } from "react-router-dom";

const About = () => <h1>About page</h1>;
const Home = () => <h1>Home page</h1>;
const Contact = () => {
  const { name } = useParams();

  return <h1 data-testid="contact-name">{name}</h1>;
};

const TestRouter = () => {
  const name = "John Doe";

  return (
    <> <nav data-testid="navbar"> <Link data-testid="home-link" to="/"> Home </Link> <Link data-testid="about-link" to="/about"> About </Link> <Link data-testid="contact-link" to={`/contact/${name}`}> Contact </Link> </nav> <Switch> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/about:name" component={Contact} /> </Switch> </>
  );
};

export default TestRouter;
复制代码

在这里,咱们有一些导航主页时想要渲染的组件。

测试导航切换时是否正确渲染

  • TestRouter.test.js :
import React from "react";
import { Router } from "react-router-dom";
import { render, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { createMemoryHistory } from "history";
import TestRouter from "./TestRouter";

const renderWithRouter = (component) => {
  const history = createMemoryHistory();
  return {
    ...render(<Router history={history}>{component}</Router>),
  };
};

it("should render the home page", () => {
  const { container, getByTestId } = renderWithRouter(<TestRouter />);
  const navbar = getByTestId("navbar");
  const link = getByTestId("home-link");

  expect(container.innerHTML).toMatch("Home page");
  expect(navbar).toContainElement(link);
});
复制代码

要测试 React Router,咱们首先必须有一个导航 history。所以,咱们使用 createMemoryHistory()  来建立导航 history 。

接下来,咱们使用辅助函数 renderWithRouter()  渲染组件并将 history  传递给 Router  组件。 这样,咱们如今能够测试在开始时加载的页面是不是主页,并在导航栏中渲染预期中的 Link  组件。

单击连接时,测试它是否导航到其余页面

  • TestRouter.test.js (将如下代码追加到该文件中):
it("should navigate to the about page", () => {
  const { container, getByTestId } = renderWithRouter(<TestRouter />);

  fireEvent.click(getByTestId("about-link"));
  expect(container.innerHTML).toMatch("About page");
});

it("should navigate to the contact page with the params", () => {
  const { container, getByTestId } = renderWithRouter(<TestRouter />);

  fireEvent.click(getByTestId("contact-link"));
  expect(container.innerHTML).toMatch("John Doe");
});
复制代码

要检查导航是否有效,咱们必须在导航连接上触发 click 事件。

对于第一个测试,咱们检查内容是否与“About Page”中的文本相等,对于第二个测试,咱们测试路由参数并检查其是否正确传递。

如今,咱们能够转到最后一节,学习如何测试 Axios 请求。

咱们快完成了!加油啊!

8.测试 HTTP Request

像往常同样,让咱们首先看一下 components/TextAxios.js  文件内容。

  • TestAxios.js :
import React from "react";
import axios from "axios";

const TestAxios = ({ url }) => {
  const [data, setData] = React.useState();

  const fetchData = async () => {
    const response = await axios.get(url);
    setData(response.data.greeting);
  };

  return (
    <> <button onClick={fetchData} data-testid="fetch-data"> Load Data </button> {data ? ( <div data-testid="show-data">{data}</div> ) : ( <h1 data-testid="loading">Loading...</h1> )} </>
  );
};

export default TestAxios;
复制代码

如你所见,咱们有一个简单的组件,该组件带有一个用于发出请求的按钮。而且若是数据不可用,它将显示一条加载中的消息(Loading...)。

如今,让咱们编写测试。

测试是否已正确提取和显示数据

  • TestAxios.test.js :
import React from "react";
import { render, waitForElement, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import axiosMock from "axios";
import TestAxios from "./TestAxios";

jest.mock("axios");

it("should display a loading text", () => {
  const { getByTestId } = render(<TestAxios />);

  expect(getByTestId("loading")).toHaveTextContent("Loading...");
});

it("should load and display the data", async () => {
  const url = "/greeting";
  const { getByTestId } = render(<TestAxios url={url} />);

  axiosMock.get.mockResolvedValueOnce({
    data: { greeting: "hello there" },
  });

  fireEvent.click(getByTestId("fetch-data"));

  const greetingData = await waitForElement(() => getByTestId("show-data"));

  expect(axiosMock.get).toHaveBeenCalledTimes(1);
  expect(axiosMock.get).toHaveBeenCalledWith(url);
  expect(greetingData).toHaveTextContent("hello there");
});
复制代码

这个测试用例有些不一样,由于咱们必须处理一个 HTTP 请求。为此,咱们必须借助 jest.mock('axios')  模拟 axios 请求。

如今,咱们可使用 axiosMock  并对其应用 get()  方法。最后,咱们将使用 Jest 的内置函数 mockResolvedValueOnce()  将模拟数据做为参数传递。

对于第二个测试,咱们能够单击按钮来获取数据,因此须要使用 async/await  来处理异步请求。如今咱们必须保证如下 3  个测试经过:

  • HTTP 请求执行了正确的次数?
  • HTTP 请求是否已经过 url 完成?
  • 获取的数据是否符合指望?

对于第一个测试,咱们只检查没有数据要显示时是否显示加载消息(loading...)。

到如今为止,咱们如今已经使用了 8  个简单步骤完成了 React Apps 的测试了。

推荐阅读

如今的你是否已经感受入门了呢?请查阅更多文档信息进阶吧,如下是一些推荐阅读:

官方文档

React Testing Library docs
React Testing Library Cheatsheet
Jest DOM matchers cheatsheet
Jest Docs

基础入门

Testing with react-testing-library and Jest

前端自动化测试 jest 教程 1-配置安装
前端自动化测试 jest 教程 2-匹配器 matchers
前端自动化测试 jest 教程 3-命令行工具
前端自动化测试 jest 教程 4-异步代码测试
前端自动化测试 jest 教程 5-钩子函数
前端自动化测试 jest 教程 6-mock 函数
前端自动化测试 jest 教程 7-定时器测试
前端自动化测试 jest 教程 8-snapshot 快照测试

写在最后

React Testing Library 是用于测试 React 组件的出色插件包。它使咱们可以访问 jest-dom 的 matcher,咱们可使用它们来更有效地并经过良好实践来测试咱们的组件,但愿本文对你有所帮助。

感谢您阅读!

这是个人 github/blog,若对你有所帮助,赏个小小的 star 🌟 咯~

相关文章
相关标签/搜索