原文链接:How to Start Testing Your React Apps Using the React Testing Library and Jest
写测试通常都会被认作一个乏味的过程,但是这是你必须掌握的一个技能,虽然在某些时候,测试并不是必要的。然后对于大多数有追求的公司而言,单元测试是必须的,开发者对于代码的自信会大幅提高,侧面来说也能提高公司对其产品的信心,也能让用户使用得更安心。
在 React 世界中,我们使用 react-testing-library 和 jest 配合使用来测试我们的 React Apps。
在本文中,我将向你介绍如何使用 8 种简单的方式来来测试你的 React App。
先备条件
本教程假定你对 React 有一定程度的了解,本教程只会专注于单元测试。
接下来,在终端中运行以下命令来克隆已经集成了必要插件的项目:
git clone https://github.com/ibrahima92/prep-react-testing-library-guide
安装依赖:
npm install
或者使用 Yarn :
yarn
好了,就这些,现在让我们了解一些基础知识!
基础知识
本文将大量使用一些关键内容,了解它们的作用可以帮助你快速理解。
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 🌟 咯~