跳至主要内容

常见问题解答

有关非 React 测试的具体问题,请参见 主要常见问题解答

如何测试输入的 onChange 处理程序?

简而言之:转到 on-change.js 示例

总结

import React from 'react'
import {render, fireEvent} from '@testing-library/react'

test('change values via the fireEvent.change method', () => {
const handleChange = jest.fn()
const {container} = render(<input type="text" onChange={handleChange} />)
const input = container.firstChild
fireEvent.change(input, {target: {value: 'a'}})
expect(handleChange).toHaveBeenCalledTimes(1)
expect(input.value).toBe('a')
})

test('select drop-downs must use the fireEvent.change', () => {
const handleChange = jest.fn()
const {container} = render(
<select onChange={handleChange}>
<option value="1">1</option>
<option value="2">2</option>
</select>,
)
const select = container.firstChild
const option1 = container.getElementsByTagName('option').item(0)
const option2 = container.getElementsByTagName('option').item(1)

fireEvent.change(select, {target: {value: '2'}})

expect(handleChange).toHaveBeenCalledTimes(1)
expect(option1.selected).toBe(false)
expect(option2.selected).toBe(true)
})

test('checkboxes (and radios) must use fireEvent.click', () => {
const handleChange = jest.fn()
const {container} = render(<input type="checkbox" onChange={handleChange} />)
const checkbox = container.firstChild
fireEvent.click(checkbox)
expect(handleChange).toHaveBeenCalledTimes(1)
expect(checkbox.checked).toBe(true)
})

如果您使用过 enzyme 或 React 的 TestUtils,您可能习惯于像这样更改输入

input.value = 'a'
Simulate.change(input)

我们无法在 React 测试库中这样做,因为 React 实际上跟踪每次您在 input 上分配 value 属性的时间,因此当您触发 change 事件时,React 认为该值实际上没有改变。

这对于 Simulate 有效,因为它们使用内部 API 来触发特殊的模拟事件。使用 React 测试库,我们尝试避免实现细节,使您的测试更具弹性。

因此,我们已经为 change 事件处理程序制定了方法,以在 React 不可跟踪的方式为您设置属性。这就是为什么您必须在 change 方法调用中传递值的原因。

我可以用这个库编写单元测试吗?

绝对可以!您可以使用这个库编写单元和集成测试。有关如何模拟依赖项(因为这个库有意不支持浅层渲染)的更多信息,请参见下文,如果您想对高级组件进行单元测试。本项目的测试展示了使用该库进行单元测试的几个示例。

在编写测试时,请记住

您的测试越类似于软件的使用方式,它们就能带给您越多的信心。 - 2018 年 2 月 17 日

如何测试组件或 Hook 中抛出的错误?

如果组件在渲染过程中抛出异常,则如果包裹在 act 中,状态更新的来源将抛出异常。默认情况下,renderfireEvent 都包裹在 act 中。您可以简单地将其包裹在 try-catch 块中,或者如果您的测试运行程序支持这些,则可以使用专门的匹配器。例如,在 Jest 中,您可以使用 toThrow

function Thrower() {
throw new Error('I throw')
}

test('it throws', () => {
expect(() => render(<Thrower />)).toThrow('I throw')
})

这同样适用于 Hooks 和 renderHook

function useThrower() {
throw new Error('I throw')
}

test('it throws', () => {
expect(() => renderHook(useThrower)).toThrow('I throw')
})
信息

React 18 将使用扩展的错误消息调用 console.error。React 19 将使用扩展的错误消息调用 console.warn,除非状态更新包裹在 act 中。renderrenderHookfireEvent 默认情况下包裹在 act 中。

如果我不能使用浅层渲染,如何在测试中模拟组件?

通常,您应该避免模拟组件(参见 指南原则部分)。但是,如果您需要,请尝试使用 Jest 的模拟功能。我发现模拟特别有用的一个情况是动画库。我不希望我的测试等待动画结束。

jest.mock('react-transition-group', () => {
const FakeTransition = jest.fn(({children}) => children)
const FakeCSSTransition = jest.fn(props =>
props.in ? <FakeTransition>{props.children}</FakeTransition> : null,
)
return {CSSTransition: FakeCSSTransition, Transition: FakeTransition}
})

test('you can mock things with jest.mock', () => {
const {getByTestId, queryByTestId} = render(
<HiddenMessage initialShow={true} />,
)
expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
// hide the message
fireEvent.click(getByTestId('toggle-message'))
// in the real world, the CSSTransition component would take some time
// before finishing the animation which would actually hide the message.
// So we've mocked it out for our tests to make it happen instantly
expect(queryByTestId('hidden-message')).toBeNull() // we just care it doesn't exist
})

请注意,因为它们是 Jest 模拟函数(jest.fn()),如果您愿意,也可以对它们进行断言。

打开完整测试 以查看完整示例。

这看起来比浅层渲染更费力(实际上也确实如此),但只要您的模拟足够接近您要模拟的对象,它就能带给您更多的信心。

如果您想让它更像浅层渲染,那么您可以做一些事情 像这样

从我的博客文章中了解有关 Jest 模拟工作原理的更多信息:"但实际上,JavaScript 模拟是什么?"

enzyme 中的“充斥着复杂性和功能”以及“鼓励糟糕的测试实践”是什么?

大多数破坏性功能都与鼓励测试实现细节有关。主要是 浅层渲染、允许通过组件构造函数选择渲染元素的 API,以及允许您获取和交互组件实例(及其状态/属性)的 API(enzyme 的大多数包装器 API 都允许这样做)。

该库的指导原则是

您的测试越类似于软件的使用方式,它们就能带给您越多的信心。 - 2018 年 2 月 17 日

因为用户无法直接与应用程序的组件实例交互、断言其内部状态或渲染的组件,或者调用其内部方法,所以在测试中执行这些操作会降低它们能够带给您的信心。

这并不是说这些操作没有用例,所以它们应该是可以实现的,只是不应该是测试 React 组件的默认和自然方式。

为什么快照差异不起作用?

如果您使用 snapshot-diff 库保存快照差异,它将无法开箱即用,因为该库使用的是 DOM,它是可变的。更改不会返回新对象,因此 snapshot-diff 将认为它是同一个对象,并避免对其进行差异化。

幸运的是,有一种简单的方法可以使其正常工作:在将 DOM 传递到 snapshot-diff 时克隆它。它看起来像这样

const firstVersion = container.cloneNode(true)
// Do some changes
snapshotDiff(firstVersion, container.cloneNode(true))
如何修复“更新未包裹在 act(...) 中”的警告?

此警告通常是由异步操作在测试已经完成之后导致更新引起的。有两种方法可以解决它

  1. 使用其中一个 异步实用程序(如 waitForfind* 查询)在测试中等待操作的结果。例如:const userAddress = await findByLabel(/address/i)
  2. 模拟异步操作,使其不触发状态更新。

一般来说,方法 1 是首选,因为它更符合用户与应用程序交互的预期。

此外,您可能会发现 这篇博客文章 在您考虑如何最好地编写能够带给您信心并避免这些警告的测试时有所帮助。

我应该在组件树的哪个级别进行测试?子级、父级,还是两者?

遵循该库的指导原则,围绕用户如何体验和交互应用程序功能,而不是围绕特定组件本身来组织测试很有用。例如,对于可重用组件库,将开发人员包含在要测试的用户列表中并单独测试每个可重用组件可能很有用。在其他情况下,组件树的具体细分只是一个实现细节,单独测试树中的每个组件可能会导致问题(参见 https://kentcdodds.com/blog/avoid-the-test-user)。

实际上,这意味着通常最好在足够高的组件树级别进行测试,以模拟真实的用户交互。关于是否值得在此基础上额外测试更高或更低级别的组件,这个问题归结为权衡问题,以及什么能够以最小的成本提供足够的价值(参见 https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests 有关不同测试级别的更多信息)。

有关此主题的更深入讨论,请参见 此视频