在使用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
})
})
发表评论
所有评论(0)