使用 React Testing Library 和 Jest 开始测试 React Apps 的 8 个简单步骤
测试通常被认为是一件乏味的事情。它只是额外的代码,而且在某些情况下,说实话,它并非必需。然而,每个开发人员都应该至少了解一些测试的基础知识,因为它能增强对产品的信心,而且对大多数公司来说,测试是一项必需技能。
在 React 的世界里,有一个很棒的库叫做 Jest,react-testing-library
它可以帮助你结合 Jest 更高效地测试 React 应用。
在本文中,我们将学习 8 个简单的步骤,让你像老板一样开始测试你的 React 应用。
最初发布在我的博客上
- 先决条件
- 基础知识
- 什么是 React 测试库?
- 1.如何创建测试快照?
- 2. 测试 DOM 元素
- 3. 测试事件
- 4. 测试异步操作
- 5. 测试 React Redux
- 6. 测试 React Context
- 7. 测试 React Router
- 8.测试HTTP请求(axios)
- 最后的想法
- 后续步骤
先决条件
本教程假设您至少对 React 有基本的了解。我将只关注测试部分。
要继续学习,您需要在终端中运行以下命令来克隆项目:
git clone https://github.com/ibrahima92/prep-react-testing-library-guide
接下来运行:
yarn
或者,如果您使用 NPM:
npm install
就是这样,让我们深入了解一些基础知识。
基础知识
一些关键的东西会在本文中大量用到,了解它们的作用可以帮助你理解。
-
it or test
:描述测试本身。它以测试名称和保存测试的函数作为参数。 -
expect
:这是测试需要通过的条件。它会将接收到的参数与匹配器进行比较。 -
a 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 />)
匹配( jest-domtoMatchSnapshot()
提供的匹配器)。顺便说一下,该方法返回了几个可以用来测试功能的方法。我们还使用了解构来获取方法。render
话虽如此,让我们继续并在下一部分中定义 React 测试库。
什么是 React 测试库?
React Testing Library 是一个轻量级的库,由Kent C. Dodds创建。它是Enzyme的替代品,并在react-dom
和之上提供了轻量级的实用函数react-dom/test-utils
。React Testing Library 是一个 DOM 测试库,这意味着它不是处理渲染的 React 组件实例,而是处理 DOM 元素及其在真实用户面前的行为。这是一个我非常喜欢的优秀库,它(相对)容易上手,鼓励良好的测试实践,而且你也可以在不使用 Jest 的情况下使用它。
“你的测试越接近软件的使用方式,它们就越能给你信心。”
那么,让我们在下一节开始使用它。顺便说一下,您不需要安装任何软件包,因为它create-react-app
已经自带了库及其依赖项。
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
在每次测试后清理所有内容,以避免内存泄漏。
接下来,我们可以用该方法渲染 App 组件,render
并将其作为返回值返回asFragment
。最后,期望 App 组件的片段与快照匹配。
现在,要运行测试,打开终端并导航到项目的根目录并运行以下命令:
yarn test
或者,如果你使用 npm:
npm test
结果,它将创建一个新文件夹__snapshots__
和一个文件App.test.js.snap
,src
如下所示:
App.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Take a snapshot should take a snapshot 1`] = `
<DocumentFragment>
<div class="App">
<h1>Testing</h1>
</div>
</DocumentFragment>
`;
如果您在 中进行其他更改App.js
,测试将失败,因为快照不再符合条件。要使其通过,只需按u
进行更新。您将在 中获得更新后的快照App.test.js.snap
。
现在,让我们继续并开始测试我们的元素。
2. 测试 DOM 元素
为了测试我们的 DOM 元素,我们首先必须查看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
。它将用于从测试文件中选择这些元素。现在,让我们编写单元测试:
-
测试计数器是否等于 0
-
TestElements.test.js
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import TestElements from './TestElements'
afterEach(cleanup);
it('should equal to 0', () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId('counter')).toHaveTextContent(0)
});
如你所见,语法与上一个测试非常相似。唯一的区别在于,我们使用getByTestId
来选择所需的元素(记住data-testid
),并检查它是否通过了测试。换句话说,我们检查文本内容是否<h1 data-testid="counter">{ counter }</h1>
等于 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
选择元素,并首先检查按钮是否具有disabled
属性。然后检查按钮是否被禁用。
如果您保存文件或在终端中再次运行yarn test
,测试将通过。
恭喜!您的第一个测试已通过!

现在,让我们在下一节中学习如何测试事件。
3. 测试事件
在编写单元测试之前,让我们首先检查一下它是什么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 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()
检查点击按钮时计数器是否增加到 1。
第二个,检查单击按钮时计数器是否减少到 -1。
fireEvent
有几种方法可以用来测试事件,请随意深入了解文档。
现在,我们知道了如何测试事件,让我们继续在下一节学习如何处理异步操作。
4. 测试异步操作
异步操作是需要时间才能完成的操作。它可以是 HTTP 请求、计时器等。
现在,让我们检查一下该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.5 秒。
-
测试计数器是否在 0.5 秒后递增。
-
TestAsync.test.js
import React from 'react';
import { render, cleanup, fireEvent, waitForElement } from '@testing-library/react';
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()
,不同之处在于getByText()
选择文本内容而不是id或data-testid。
现在,点击按钮后,我们等待计数器增加waitForElement(() => getByText('1'))
。一旦计数器增加到 1,我们就可以转到条件并检查计数器是否有效等于 1。
话虽如此,现在让我们转向更复杂的测试用例。
你准备好了吗?

5. 测试 React Redux
如果你是 React Redux 的新手,这篇文章可能会对你有所帮助。否则,我们先来看看它是什么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 { 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()
接收待渲染的组件、初始状态和 store 作为参数。
如果没有 store,则会创建一个新的;如果没有接收到初始状态或 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')
})
为了测试递增和递减事件,我们将初始状态作为第二个参数传递给renderWithRedux()
。现在,我们可以点击按钮并测试预期结果是否符合条件。
现在,让我们进入下一部分并介绍 React Context。
接下来是 React Router 和 Axios,你还在关注我吗?

6. 测试 React Context
如果你是 React Context 的新手,请先阅读本文。否则,我们来查看TextContext.js
文件。
TextContext.js
import React from "react"
export const CounterContext = React.createContext()
const CounterProvider = () => {
const [counter, setCounter] = React.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 } = React.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
-
TextContext.test.js
import React from 'react'
import { render, cleanup, fireEvent } from '@testing-library/react'
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。
-
测试计数器是否正确递增和递减。
-
TextContext.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')
})
如您所见,这里我们触发一个点击事件来测试计数器是否正确增加到 1 并减少到 -1。
话虽如此,我们现在可以进入下一部分并介绍 React Router。
7. 测试 React Router
如果你想深入了解 React Router,这篇文章或许能帮到你。否则,我们来看一下这个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 { 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,我们首先需要有一个导航历史记录。因此,我们使用createMemoryHistory()
以及猜到的名称来创建导航历史记录。
接下来,我们使用辅助函数renderWithRouter()
来渲染组件并将其传递history
给Router
组件。这样,我们现在可以测试开始时加载的页面是否是主页。以及导航栏是否加载了预期的链接。
-
测试点击链接时是否能通过参数导航到其他页面
-
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')
})
现在,为了检查导航是否有效,我们必须在导航链接上触发点击事件。
对于第一个测试,我们检查内容是否等于“关于”页面中的文本,对于第二个测试,我们测试路由参数并检查它是否正确传递。
现在我们可以进入最后一部分,学习如何测试 Axios 请求。
我们快完成了

8.测试HTTP请求(axios)
像往常一样,我们首先看看TextAxios.js
文件是什么样子的。
TextAxios.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
正如你所见,我们有一个简单的组件,它有一个按钮用于发出请求。如果数据不可用,它将显示一条加载消息。
现在,让我们编写测试。
-
测试数据是否被正确获取和显示。
-
TextAxios.test.js
import React from 'react'
import { render, waitForElement, fireEvent } from '@testing-library/react'
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 请求。为此,我们必须借助 来模拟 axios 请求jest.mock('axios')
。
现在,我们可以使用axiosMock
并应用一种get()
方法,最后使用 Jest 函数mockResolvedValueOnce()
将模拟数据作为参数传递。
这样,我们现在可以进行第二个测试,点击按钮获取数据,并使用 async/await 来解析。现在我们需要测试三件事:
- 如果 HTTP 请求正确完成
- 如果 HTTP 请求已通过
url
- 如果获取的数据符合预期。
对于第一个测试,我们只是检查当没有数据可显示时是否显示加载消息。
话虽如此,我们现在已经完成了开始测试 React 应用程序的 8 个简单步骤。
不再害怕测试

最后的想法
React Testing Library 是一个很棒的 React 应用测试包。它允许我们使用jest-dom
匹配器,从而更高效地测试组件,并遵循良好的实践。希望本文对您有所帮助,并帮助您在未来构建健壮的 React 应用。
您可以在这里找到完成的项目
谢谢阅读!