原文连接:How to Start Testing Your React Apps Using the React Testing Library and Jestjavascript
写测试一般都会被认做一个乏味的过程,可是这是你必须掌握的一个技能,虽然在某些时候,测试并非必要的。而后对于大多数有追求的公司而言,单元测试是必须的,开发者对于代码的自信会大幅提升,侧面来讲也能提升公司对其产品的信心,也能让用户使用得更安心。html
在 React 世界中,咱们使用 react-testing-library 和 jest 配合使用来测试咱们的 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 组件的很是便捷的解决方案。 它在 react-dom
和 react-dom/test-utils
之上提供了轻量且实用的 API,若是你打开 React 官网中的测试工具推荐,你会发现 Note 中写了:
注意: 咱们推荐使用 React Testing Library,它使得针对组件编写测试用例就像终端用户在使用它同样方便。
React Testing Library 是一个 DOM 测试库,这意味着它并不会直接处理渲染的 React 组件实例,而是处理 DOM 元素以及它们在实际用户面前的行为。
这是一个很棒的库,(相对)易于使用,而且鼓励良好的测试实践。 固然,你也能够在没有 Jest 的状况下使用它。
“你的测试与软件的使用方式越接近,就能越给你信心。”
那么,让咱们在下一部分中就开始使用它吧。顺便说一下,你不须要安装任何依赖了,刚才克隆的项目自己是用 create-react-app
建立的,已经集成了编写单元测试所须要的插件了,只需保证你已经安装了依赖便可。
顾名思义,快照使咱们能够保存给定组件的快照。 当你对组件进行一些更新或重构,但愿获取或比较更改时,它会颇有帮助。
如今,让咱们对 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
中拥有更新后的快照。
如今,让咱们继续并开始测试咱们的元素。
为了测试咱们的 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 元素。 如今,让咱们编写单元测试:
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
。
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 元素处于禁用状态。
保存以后再运行测试命令,你会发现测试所有经过了!
恭喜你成功经过了本身的第一个测试!
如今,让咱们在下一部分中学习如何测试事件。
在写单元测试以前,咱们先来看看 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
有几种可用于测试事件的方法,所以请随时阅读文档以了解更多信息。
如今咱们知道了如何测试事件,让咱们继续学习下一节如何处理异步操做。
异步操做须要花费一些时间才能完成。它能够是 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()
模拟异步。
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
。
是否是理解起来很简单?话虽如此,让咱们如今转到更复杂的测试用例。
你准备好了吗?
若是您不熟悉 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 处理的基本计数器组件。
如今,让咱们编写单元测试。
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,你还会看下去吗?
若是您不熟悉 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 进行管理,让咱们编写单元测试以检查其行为是否符合预期。
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。
若是您想深刻研究 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 请求。
咱们快完成了!加油啊!
像往常同样,让咱们首先看一下 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
个测试经过:
对于第一个测试,咱们只检查没有数据要显示时是否显示加载消息(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 🌟 咯~