使用 React Testing Library 和 Jest 开始测试 React Apps 的 8 个简单步骤

2025-05-28

使用 React Testing Library 和 Jest 开始测试 React Apps 的 8 个简单步骤

测试通常被认为是一件乏味的事情。它只是额外的代码,而且在某些情况下,说实话,它并非必需。然而,每个开发人员都应该至少了解一些测试的基础知识,因为它能增强对产品的信心,而且对大多数公司来说,测试是一项必需技能。
在 React 的世界里,有一个很棒的库叫做 Jest,react-testing-library它可以帮助你结合 Jest 更高效地测试 React 应用。
在本文中,我们将学习 8 个简单的步骤,让你像老板一样开始测试你的 React 应用。

最初发布在我的博客上

先决条件

本教程假设您至少对 React 有基本的了解。我将只关注测试部分。
要继续学习,您需要在终端中运行以下命令来克隆项目:

  git clone https://github.com/ibrahima92/prep-react-testing-library-guide
Enter fullscreen mode Exit fullscreen mode

接下来运行:

  yarn
Enter fullscreen mode Exit fullscreen mode

或者,如果您使用 NPM:

npm install
Enter fullscreen mode Exit fullscreen mode

就是这样,让我们​​深入了解一些基础知识。

基础知识

一些关键的东西会在本文中大量用到,了解它们的作用可以帮助你理解。

  • 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()
   })
});
Enter fullscreen mode Exit fullscreen mode

如您所见,我们用 描述测试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()
   })
});
Enter fullscreen mode Exit fullscreen mode

要获取快照,我们首先必须导入rendercleanup。这两个方法将在本文中大量使用。render,正如你可能猜到的那样,它有助于渲染 React 组件。 和cleanup作为参数传递给 ,用于afterEach在每次测试后清理所有内容,以避免内存泄漏。

接下来,我们可以用该方法渲染 App 组件,render并将其作为返回值返回asFragment。最后,期望 App 组件的片段与快照匹配。

现在,要运行测试,打开终端并导航到项目的根目录并运行以下命令:

  yarn test
Enter fullscreen mode Exit fullscreen mode

或者,如果你使用 npm:

  npm test
Enter fullscreen mode Exit fullscreen mode

结果,它将创建一个新文件夹__snapshots__和一个文件App.test.js.snapsrc如下所示:

  • 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>
`;
Enter fullscreen mode Exit fullscreen mode

如果您在 中进行其他更改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
Enter fullscreen mode Exit fullscreen mode

这里,你唯一需要保留的是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)
   });
Enter fullscreen mode Exit fullscreen mode

如你所见,语法与上一个测试非常相似。唯一的区别在于,我们使用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()
  });
Enter fullscreen mode Exit fullscreen mode

这里,像往常一样,我们使用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
Enter fullscreen mode Exit fullscreen mode

现在,让我们编写测试。

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

  • 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')
  });

Enter fullscreen mode Exit fullscreen mode

如您所见,除了预期的文本内容外,这两个测试非常相似。

第一个测试触发点击事件,以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
Enter fullscreen mode Exit fullscreen mode

这里我们用来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')
  });
Enter fullscreen mode Exit fullscreen mode

为了测试增量事件,我们首先必须使用 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)
Enter fullscreen mode Exit fullscreen mode

并用于减速器。

  • 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
    }
  }
Enter fullscreen mode Exit fullscreen mode

正如您所见,没有什么特别之处,它只是一个由 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')
  })
Enter fullscreen mode Exit fullscreen mode

为了测试 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')
})
Enter fullscreen mode Exit fullscreen mode

为了测试递增和递减事件,我们将初始状态作为第二个参数传递给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
Enter fullscreen mode Exit fullscreen mode

现在,计数器状态通过 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')
})
Enter fullscreen mode Exit fullscreen mode

与上一节关于 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')
  })
Enter fullscreen mode Exit fullscreen mode

如您所见,这里我们触发一个点击事件来测试计数器是否正确增加到 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
Enter fullscreen mode Exit fullscreen mode

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

现在,让我们编写测试

  • 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)
})
Enter fullscreen mode Exit fullscreen mode

要测试 React Router,我们首先需要有一个导航历史记录。因此,我们使用createMemoryHistory()以及猜到的名称来创建导航历史记录。

接下来,我们使用辅助函数renderWithRouter()来渲染组件并将其传递historyRouter组件。这样,我们现在可以测试开始时加载的页面是否是主页。以及导航栏是否加载了预期的链接。

  • 测试点击链接时是否能通过参数导航到其他页面

  • 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')
})
Enter fullscreen mode Exit fullscreen mode

现在,为了检查导航是否有效,我们必须在导航链接上触发点击事件。

对于第一个测试,我们检查内容是否等于“关于”页面中的文本,对于第二个测试,我们测试路由参数并检查它是否正确传递。

现在我们可以进入最后一部分,学习如何测试 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
Enter fullscreen mode Exit fullscreen mode

正如你所见,我们有一个简单的组件,它有一个按钮用于发出请求。如果数据不可用,它将显示一条加载消息。

现在,让我们编写测试。

  • 测试数据是否被正确获取和显示。

  • 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')
})
Enter fullscreen mode Exit fullscreen mode

这个测试用例有点不同,因为我们必须处理 HTTP 请求。为此,我们必须借助 来模拟 axios 请求jest.mock('axios')

现在,我们可以使用axiosMock并应用一种get()方法,最后使用 Jest 函数mockResolvedValueOnce()将模拟数据作为参数传递。

这样,我们现在可以进行第二个测试,点击按钮获取数据,并使用 async/await 来解析。现在我们需要测试三件事:

  1. 如果 HTTP 请求正确完成
  2. 如果 HTTP 请求已通过url
  3. 如果获取的数据符合预期。

对于第一个测试,我们只是检查当没有数据可显示时是否显示加载消息。

话虽如此,我们现在已经完成了开始测试 React 应用程序的 8 个简单步骤。

不再害怕测试

不害怕

最后的想法

React Testing Library 是一个很棒的 React 应用测试包。它允许我们使用jest-dom匹配器,从而更高效地测试组件,并遵循良好的实践。希望本文对您有所帮助,并帮助您在未来构建健壮的 React 应用。

您可以在这里找到完成的项目

谢谢阅读!

后续步骤

React 测试库文档

React 测试库备忘单

Jest DOM 匹配器速查表

Jest 文档

文章来源:https://dev.to/ibrahima92/8-simple-steps-to-start-testing-react-apps-using-react-testing-library-and-jest-3922
PREV
编写良好的单元测试:循序渐进教程 正面案例 极端案例 负面案例 ExampleUnitTests 教程总结
NEXT
自定义 React Hooks:useBoolean