Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
vortesnail opened this issue Oct 23, 2020 · 0 comments
Open

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

vortesnail opened this issue Oct 23, 2020 · 0 comments
Labels

Comments

@vortesnail
Copy link
Owner

原文链接:How to Start Testing Your React Apps Using the React Testing Library and Jest

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

在 React 世界中,我们使用 react-testing-libraryjest 配合使用来测试我们的 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 🌟咯~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant