Web全栈开发学习笔记—Part5 测试 React 应用—c.测试React 应用

目录

Running tests

Test file location

Searching for content in a component

Debugging tests

Clicking buttons in tests

Tests for the Togglable component

Testing the forms

Test coverage

Frontend integration tests

Snapshot testing


除了 Jest 之外还需要另一个测试库帮助我们以测试目的渲染组件。 目前最好的选择是react-testing-library ,用以下命令来安装:

npm install --save-dev @testing-library/react @testing-library/jest-dom

让我们首先为负责渲染 Note 的组件编写测试:

const Note = ({ note, toggleImportance }) => {
  const label = note.important
    ? 'make not important'
    : 'make important'

  return (
    <li className='note'>      {note.content}
      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

注意,li 元素具有 CSS classname note ,用于访问我们测试中的组件。

Running tests

【运行测试】

Create-react-app 默认情况下将测试配置为在 watch 模式下运行,这意味着 npm test 命令在测试结束后不会退出,而是等待对代码进行更改。 一旦保存了对代码的新的更改,测试就会自动执行,然后 Jest 等待新的更改。

如果想“正常地”运行测试,可以使用以下命令:

CI=true npm test

注意: 如果您没有安装 Watchman,控制台可能会发出警告。 Watchman 是 Facebook 开发的一个应用程序,可以监视文件的变化。 这个程序加快了测试的执行速度,至少从 macOS Sierra 开始,在 watch 模式下运行测试会向控制台发出一些警告,这些警告可以通过安装 Watchman 来消除。

Test file location

在 React 中,关于测试文件的位置有不同的约定 。 我们当前的标准是创建测试文件,将它们放在与被测试组件相同的目录中。

另一个约定是将测试文件存储在它们自己的单独目录中。 

Searching for content in a component

【在组件中搜寻内容】

react-testing-library 包提供了许多不同的方法来研究被测试组件的内容。 稍微扩展一下测试:

test('renders content', () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  const component = render(
    <Note note={note} />
  )

  // method 1
  expect(component.container).toHaveTextContent(
    'Component testing is done with react-testing-library'
  )

  // method 2
  const element = component.getByText(
    'Component testing is done with react-testing-library'
  )
  expect(element).toBeDefined()

  // method 3
  const div = component.container.querySelector('.note')
  expect(div).toHaveTextContent(
    'Component testing is done with react-testing-library'
  )
})

第一种方法是使用toHaveTextContent方法从组件渲染的整个 HTML 代码中搜索匹配的文本。

toHaveTextContent 是许多“匹配器”方法之一,由jest-dom库提供的。

第二种方法使用 render 方法返回对象的getByText 。 该方法返回包含给定文本的元素。如果不存在此类元素,则发生异常。 出于这个原因,在技术上不需要指定任何额外的except。

第三种方法是搜索由组件渲染的特定元素,该组件使用querySelector方法,该方法接收CSS 选择器作为其参数。

最后两个方法使用getByText 和querySelector 方法从渲染的组件中查找匹配某些条件的元素。

Debugging tests

【调试测试】

在编写测试时,通常会遇到许多不同类型的问题。

由 render 方法返回的对象具有一个调试方法debug ,该方法可用于将组件渲染的 HTML 打印到控制台。对代码进行以下更改:

test('renders content', () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  const component = render(
    <Note note={note} />
  )

  component.debug()
  // ...
})

可以在控制台中看到由组件生成的 HTML:

console.log node_modules/@testing-library/react/dist/index.js:90
  <body>
    <div>
      <li
        class="note"
      >
        Component testing is done with react-testing-library
        <button>
          make not important
        </button>
      </li>
    </div>
  </body>

还可以搜索组件的一小部分并打印其 HTML 代码。 做到这一点需要 prettyDOM 方法,该方法可以从 @testing-library/dom 包中导入,该包通过 react-testing-library 自动安装了:

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render } from '@testing-library/react'
import { prettyDOM } from '@testing-library/dom'import Note from './Note'

test('renders content', () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  const component = render(
    <Note note={note} />
  )
  const li = component.container.querySelector('li')
  
  console.log(prettyDOM(li))})

使用选择器查找组件内部的 li 元素,并将其 HTML 输出到控制台:

console.log src/components/Note.test.js:21
  <li
    class="note"
  >
    Component testing is done with react-testing-library
    <button>
      make not important
    </button>
  </li>

Clicking buttons in tests

【在测试中点击按钮】

除了显示内容之外, Note 组件还确保在按下与 Note 关联的按钮时,调用 toggleImportance 事件处理函数。

测试这个功能可以这样完成:

import React from 'react'
import { render, fireEvent } from '@testing-library/react'import { prettyDOM } from '@testing-library/dom'
import Note from './Note'

// ...

test('clicking the button calls event handler once', () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  const mockHandler = jest.fn()

  const component = render(
    <Note note={note} toggleImportance={mockHandler} />
  )

  const button = component.getByText('make not important')
  fireEvent.click(button)

  expect(mockHandler.mock.calls).toHaveLength(1)
})

 事件处理程序是用 Jest 定义的 mock 函数:

const mockHandler = jest.fn()

测试根据渲染组件的文本找到按钮,然后单击元素:

const button = component.getByText('make not important')
fireEvent.click(button)

使用 fireEvent 方法进行单击。

测试的期望验证 mock function 是否只被调用了一次。

expect(mockHandler.mock.calls).toHaveLength(1)

模拟对象和函数 是测试中常用的根组件,用于替换被测试组件的依赖项。 通过 mock 可以返回硬编码的响应,并验证调用 mock 函数的次数和参数。

Tests for the Togglable component

【测试可切换组件】

让我们为 Togglable 组件编写一些测试。 让我们将 togglableContent CSS classname 添加到返回子组件的 div。

const Togglable = React.forwardRef((props, ref) => {
  // ...

  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>
          {props.buttonLabel}
        </button>
      </div>
      <div style={showWhenVisible} className="togglableContent">        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
})

测试结果如下:

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render, fireEvent } from '@testing-library/react'
import Togglable from './Togglable'

describe('<Togglable />', () => {
  let component

  beforeEach(() => {
    component = render(
      <Togglable buttonLabel="show...">
        <div className="testDiv" />
      </Togglable>
    )
  })

  test('renders its children', () => {
    expect(
      component.container.querySelector('.testDiv')
    ).toBeDefined()
  })

  test('at start the children are not displayed', () => {
    const div = component.container.querySelector('.togglableContent')

    expect(div).toHaveStyle('display: none')
  })

  test('after clicking the button, children are displayed', () => {
    const button = component.getByText('show...')
    fireEvent.click(button)

    const div = component.container.querySelector('.togglableContent')
    expect(div).not.toHaveStyle('display: none')
  })

})

beforeEach 函数在每个测试之前会被调用,然后将 Togglable 组件渲染到 component 变量中

第一个测试验证 Togglable 组件是否渲染了其子组件 <div className="testDiv" />

剩下的测试使用 toHaveStyle 方法,通过检查 div 元素的样式是否包含{ display: 'none' },来验证 Togglable 组件的子组件最初是否可见。 另一个测试验证按钮被按下时组件是可见的,这意味着隐藏组件的样式不再附着到这个组件中。

根据按钮所包含的文本再次搜索按钮。 这个按钮也可以通过 CSS 选择器来定位:

const button = component.container.querySelector('button')

该组件包含两个按钮,但由于 querySelector 返回第一个匹配按钮,因此碰巧得到了所需的按钮。

添加一个测试,通过点击组件的第二个按钮来验证可见内容是否可以隐藏:

test('toggled content can be closed', () => {
  const button = component.container.querySelector('button')
  fireEvent.click(button)

  const closeButton = component.container.querySelector(
    'button:nth-child(2)'
  )
  fireEvent.click(closeButton)

  const div = component.container.querySelector('.togglableContent')
  expect(div).toHaveStyle('display: none')
})

定义了一个选择器,它返回第二个按钮: button:nth-child(2)。建议根据文本查找元素:

test('toggled content can be closed', () => {
  const button = component.getByText('show...')
  fireEvent.click(button)

  const closeButton = component.getByText('cancel')
  fireEvent.click(closeButton)

  const div = component.container.querySelector('.togglableContent')
  expect(div).toHaveStyle('display: none')
})

Testing the forms

【测试表单】

前面的测试中使用fireEvent函数来单击按钮。

const button = component.getByText('show...')
fireEvent.click(button)

实际上,我们不仅可以使用fireEvent 为按钮组件创建click 事件,还可以使用fireEvent 来模拟文本输入。

NoteForm 组件进行测试

import React, { useState } from 'react'

const NoteForm = ({ createNote }) => {
  const [newNote, setNewNote] = useState('')

  const handleChange = (event) => {
    setNewNote(event.target.value)
  }

  const addNote = (event) => {
    event.preventDefault()
    createNote({
      content: newNote,
      important: Math.random() > 0.5,
    })

    setNewNote('')
  }

  return (
    <div className="formDiv">      <h2>Create a new note</h2>

      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleChange}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

export default NoteForm

该表单通过调用作为props接收的 createNote 函数以及新便笺的细节来工作。

测试内容如下:

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'
import NoteForm from './NoteForm'

test('<NoteForm /> updates parent state and calls onSubmit', () => {
  const createNote = jest.fn()

  const component = render(
    <NoteForm createNote={createNote} />
  )

  const input = component.container.querySelector('input')
  const form = component.container.querySelector('form')

  fireEvent.change(input, { 
    target: { value: 'testing of forms could be easier' } 
  })
  fireEvent.submit(form)

  expect(createNote.mock.calls).toHaveLength(1)
  expect(createNote.mock.calls[0][0].content).toBe('testing of forms could be easier' )
})

通过为input 字段创建一个change 事件,并定义一个包含写入字段的文本的对象,可以模拟对input 字段的写入。

表单通过模拟submit 事件发送到表单。

第一个测试期望确保提交表单调用 createNote 方法。

第二个期望检查,使用正确的参数调用事件处理程序——即在填写表单时创建具有正确内容的通知。

Test coverage

【测试覆盖范围】

通过运行如下命令,我们可以很容易地找到我们测试的覆盖范围coverage

CI=true npm test -- --coverage

fullstack content

coverage/lcov-report目录将生成相当原始的 HTML raport。

该报告会告诉我们每个组件中未经测试的代码行:

fullstack content

Frontend integration tests

【前端集成测试】

之前为后端编写了集成测试,测试其逻辑并通过后端提供的 API 连接数据库。 在编写这些测试时,我们有意地不编写单元测试,因为后端的代码相当简单,但是应用中的错误可能发生在更复杂的场景中,而单元测试非常适合这些场景。

到目前为止,对前端的所有测试都是单元测试,这些测试验证了单个组件的正确功能。单元测试有时很有用,但即使是一套完整的单元测试套件也不足以验证应用作为一个整体是否工作。

我们也可以对前端进行集成测试。集成测试可测试多组件的协作。这比单元测试要困难得多,因为必须从服务器模拟数据。

Snapshot testing

【快照测试】

Jest 提供了一种与“传统”测试完全不同的替代方法,称为snapshot。 快照测试的有趣特性是开发人员不需要自己定义任何测试,只需要采用快照测试即可。

基本原则是比较组件更改后定义的 HTML 代码和更改前存在的 HTML 代码。

如果快照注意到组件定义的 HTML 中发生了一些变化,那么它要么是新功能,要么是由于意外造成的“ bug”。 如果组件的 HTML 代码发生更改,快照测试会通知开发人员。 开发人员必须告诉 Jest 是否需要更改。 如果 HTML 代码的更改是意想不到的,那么它一定会隐含一个 bug,而由于快照测试,开发人员可以很容易地意识到这些潜在的问题。

猜你喜欢

转载自blog.csdn.net/qq_39389123/article/details/112387103