使用react-testing-library的常见的错误和不好使用习惯

在使用react-testing-library时最好安装eslint插件

注意:如果您使用的是 create-react-app,eslint-plugin-testing-library则它已作为依赖项包含在内。

这个名字
wrapper是旧的enzyme,我们在这里不需要它。

// ❌
const wrapper = render(<Example prop="1" />)
wrapper.rerender(<Example prop="2" />)

// ✅
const {rerender} = render(<Example prop="1" />)
rerender(<Example prop="2" />)

 

cleanup都是自动发生的

// ❌
import {render, screen, cleanup} from '@testing-library/react'

afterEach(cleanup)

// ✅
import {render, screen} from '@testing-library/react'

 

使用的好处screen是您不再需要在render添加/删除所需查询时使调用解构保持最新。您只需要输入screen.并让您的编辑器神奇的自动完成功能来处理其余的事情。

// ❌
const {getByRole} = render(<Example />)
const errorMessageNode = getByRole('alert')

// ✅
render(<Example />)
const errorMessageNode = screen.getByRole('alert')

 

toBeDisabled断言来自 jest-dom. 强烈建议使用它,jest-dom因为您收到的错误消息要好得多。

const button = screen.getByRole('button', {name: /disabled button/i})

// ❌
expect(button.disabled).toBe(true)
// error message:
//  expect(received).toBe(expected) // Object.is equality
//
//  Expected: true
//  Received: false

// ✅
expect(button).toBeDisabled()
// error message:
//   Received element is not disabled:
//     <button />

 

尽可能接近最终用户的查询方式来查询 DOM

// ❌
// assuming you've got this DOM to work with:
// <label>Username</label><input data-testid="username" />
screen.getByTestId('username')

// ✅
// change the DOM to be accessible by associating the label and setting the type
// <label for="username">Username</label><input id="username" type="text" />
screen.getByRole('textbox', {name: /username/i})

在本地化的情况下,我建议使用默认语言环境),而不是到处使用测试 ID 或其他机制,如果内容作者将“用户名”更改为“电子邮件”,第二种肯定会测出来。

// ❌
screen.getByTestId('submit-button')

// ✅
screen.getByRole('button', {name: /submit/i})

该name选项允许您通过元素的 “可访问名称”查询元素,这是屏幕阅读器将为元素读取的内容,即使您的元素的文本内容被不同的元素分割,它也可以工作。例如:

// assuming we've got this DOM structure to work with
// <button><span>Hello</span> <span>World</span></button>

screen.getByText(/hello world/i)
// ❌ fails with the following error:
// Unable to find an element with the text: /hello world/i. This could be
// because the text is broken up by multiple elements. In this case, you can
// provide a function for your text matcher to make your matcher more flexible.

screen.getByRole('button', {name: /hello world/i})
// ✅ works!

人们不使用*ByRole查询的一个原因是因为他们不熟悉放置在元素上的隐式角色。 这是 MDN 上的角色列表。因此,我最喜欢的查询的另一个功能*ByRole是,如果我们无法找到具有您指定角色的元素,我们不仅会像使用普通get*或find*变体一样将整个 DOM 记录给您,而且我们还会记录您可以查询的所有可用角色!

 

// ❌
fireEvent.change(input, {target: {value: 'hello world'}})

// ✅
userEvent.type(input, 'hello world')

fireEvent.change将简单地触发输入上的单个更改事件。但是,type调用也会触发每个字符的keyDown、keyPress和keyUp事件。它更接近用户的实际交互。这样做的好处是可以很好地与您可能使用的库一起工作,这些库实际上并不监听更改事件。

 

建议:仅使用query*变体来断言无法找到元素。query*是你有一个可以调用的函数,如果没有找到与查询匹配的元素,它不会抛出错误(如果没有找到元素则返回null)。这很有用的唯一 原因是验证元素没有呈现到页面。这如此重要的原因是,如果没有找到任何元素, get*andfind*变体将抛出一个非常有用的错误——它会打印出整个文档,这样您就可以看到呈现的内容以及查询未能找到所需内容的原因。

// ❌
expect(screen.queryByRole('alert')).toBeInTheDocument()

// ✅
expect(screen.getByRole('alert')).toBeInTheDocument()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

 

建议:find*在您想查询可能无法立即获得的内容的任何时候使用。这两段代码基本上是等效的(find*查询waitFor 在后台使用),但第二段代码更简单,你得到的错误信息会更好。

// ❌
const submitButton = await waitFor(() =>
  screen.getByRole('button', {name: /submit/i}),
)

// ✅
const submitButton = await screen.findByRole('button', {name: /submit/i})

 

使用querySelector我们进行查询,我们会失去很多信心,测试更难阅读,并且会更频繁地中断

// ❌
const {container} = render(<Example />)
const button = container.querySelector('.btn-primary')
expect(button).toHaveTextContent(/click me/i)

// ✅
render(<Example />)
screen.getByRole('button', {name: /click me/i})

的目的waitFor是让您等待特定的事情发生。如果您传递一个空回调,它今天可能会起作用,因为您需要等待的只是“事件循环的一个滴答声”,这要归功于您的模拟工作方式。但是你会留下一个脆弱的测试,如果你重构你的异步逻辑很容易失败。

建议:等待内部的特定断言waitFor

// ❌
await waitFor(() => {})
expect(window.fetch).toHaveBeenCalledWith('foo')
expect(window.fetch).toHaveBeenCalledTimes(1)

// ✅
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
expect(window.fetch).toHaveBeenCalledTimes(1)

建议:只在回调中放置一个断言。window.fetch它被调用了两次。所以 waitFor调用会失败,但是,我们必须等待超时才能看到测试失败。通过在其中放置一个断言,我们既可以等待 UI 稳定到我们想要断言的状态,也可以在其中一个断言最终失败时更快地失败。

// ❌
await waitFor(() => {
  expect(window.fetch).toHaveBeenCalledWith('foo')
  expect(window.fetch).toHaveBeenCalledTimes(1)
})

// ✅
await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
expect(window.fetch).toHaveBeenCalledTimes(1)

waitFor适用于在您执行的操作和断言通过之间具有不确定时间量的事物。因此,可以以不确定的次数和频率调用(或检查错误)回调(在间隔以及存在 DOM 突变时调用)。所以这意味着你的副作用可能会运行多次!

这也意味着您不能在waitFor. 如果您确实想使用快照断言,则首先等待特定断言,然后您可以拍摄快照。

建议:将副作用放在回调之外waitFor,并将回调保留用于断言。

// ❌
await waitFor(() => {
  fireEvent.keyDown(input, {key: 'ArrowDown'})
  expect(screen.getAllByRole('listitem')).toHaveLength(3)
})

// ✅
fireEvent.keyDown(input, {key: 'ArrowDown'})
await waitFor(() => {
  expect(screen.getAllByRole('listitem')).toHaveLength(3)
})

fireEvent已经被包裹了act!所以那些没有做任何有用的事情。

// ❌
act(() => {
  render(<Example />)
})

const input = screen.getByRole('textbox', {name: /choose a fruit/i})
act(() => {
  fireEvent.keyDown(input, {key: 'ArrowDown'})
})

// ✅
render(<Example />)
const input = screen.getByRole('textbox', {name: /choose a fruit/i})
fireEvent.keyDown(input, {key: 'ArrowDown'})

 

大多数情况下,如果您看到act警告,实际上告诉您测试中发生了一些意想不到的事情。此时需要为那些触发stateChange的方法包裹act。

例如:

function UsernameForm({updateUsername}) {
  const [{status, error}, setState] = React.useState({
    status: 'idle',
    error: null,
  })

  async function handleSubmit(event) {
    event.preventDefault()
    const newUsername = event.target.elements.username.value
    setState({status: 'pending'})
    try {
      await updateUsername(newUsername)
      setState({status: 'fulfilled'})
    } catch (e) {
      setState({status: 'rejected', error: e})
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Username</label>
      <input id="username" />
      <button type="submit">Submit</button>
      <span>{status === 'pending' ? 'Saving...' : null}</span>
      <span>{status === 'rejected' ? error.message : null}</span>
    </form>
  )
}
import * as React from 'react'
import user from '@testing-library/user-event'
import {render, screen} from '@testing-library/react'

test('calls updateUsername with the new username', async () => {
  const handleUpdateUsername = jest.fn()
  const fakeUsername = 'sonicthehedgehog'

  render(<UsernameForm updateUsername={handleUpdateUsername} />)

  const usernameInput = screen.getByLabelText(/username/i)
  user.type(usernameInput, fakeUsername)
  user.click(screen.getByText(/submit/i))

  expect(handleUpdateUsername).toHaveBeenCalledWith(fakeUsername)
})
console.error node_modules/react-dom/cjs/react-dom.development.js:530
  Warning: An update to UsernameForm inside a test was not wrapped in act(...).

  When testing, code that causes React state updates should be wrapped into act(...):

  act(() => {
    /* fire events that update state */
  });
  /* assert on the output */

  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
      in UsernameForm

添加 waitForElementToBeRemoved

test('calls updateUsername with the new username', async () => {
  const handleUpdateUsername = jest.fn()
  const fakeUsername = 'sonicthehedgehog'

  render(<UsernameForm updateUsername={handleUpdateUsername} />)

  const usernameInput = screen.getByLabelText(/username/i)
  user.type(usernameInput, fakeUsername)
  user.click(screen.getByText(/submit/i))

  expect(handleUpdateUsername).toHaveBeenCalledWith(fakeUsername)
  await waitForElementToBeRemoved(() => screen.queryByText(/saving/i))
})

 

另一种workaround,创建一个promise,在act中wait promise

test('calls updateUsername with the new username', async () => {
  const promise = Promise.resolve() // You can also resolve with a mocked return value if necessary
  const handleUpdateUsername = jest.fn(() => promise)
  const fakeUsername = 'sonicthehedgehog'

  render(<UsernameForm updateUsername={handleUpdateUsername} />)

  const usernameInput = screen.getByLabelText(/username/i)
  user.type(usernameInput, fakeUsername)
  user.click(screen.getByText(/submit/i))

  expect(handleUpdateUsername).toHaveBeenCalledWith(fakeUsername)
  // we await the promise instead of returning directly, because act expects a "void" result
  await act(async () => {
    await promise
  })
})

 

 

相关标签:
  • react-testing-library
  • react
0人点赞

发表评论

当前游客模式,请登陆发言

所有评论(0)