React 中自定义的 Hooks 为开发者提供了重用公共方法的能力。然而,如果你是一个测试新手的话,测试这些钩子可能会很棘手。本文中,我们将探索如何使用 React Testing Library
测试库来测试自定义钩子。
开始前,首先让我们回顾一下如何测试一个基本的 React 组件。我这里提供一个 Counter
组件的例子,该组件显示一个计数和一个按钮,当单击该按钮时,计数会增加。其中,Counter
组件接受一个名为 initialCount
的 props
,如果没有提供,该 props
默认为 0。例子的代码如下所示:
import { useState } from 'react'
type UseCounterProps = {
initialCount?: number
}
export const Counter = ({ initialCount = 0 }: CounterProps = {}) => {
const [count, setCount] = useState(initialCount)
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
如果使用 React Testing Library
测试 Counter
组件,通常需要遵循以下步骤:
Render
函数来渲染组件。screen
对象获取 DOM 元素(可以使用 ByRole
来查询元素)。@testing-library/user-event
库模拟用户事件。以下测试中,我们依据上述的步骤来验证 Counter
组件的功能:
import { render, screen } from '@testing-library/react'
import { Counter } from './Counter'
import user from '@testing-library/user-event'
describe('Counter', () => {
test('renders a count of 0', () => {
render(<Counter />)
const countElement = screen.getByRole('heading')
expect(countElement).toHaveTextContent('0')
})
test('renders a count of 1', () => {
render(<Counter initialCount={1} />)
const countElement = screen.getByRole('heading')
expect(countElement).toHaveTextContent('1')
})
test('renders a count of 1 after clicking the increment button', async () => {
user.setup()
render(<Counter />)
const incrementButton = screen.getByRole('button', { name: 'Increment' })
await user.click(incrementButton)
const countElement = screen.getByRole('heading')
expect(countElement).toHaveTextContent('1')
})
})
initialCount
的值为1,并测试呈现的计数值是否也是1。Increment
按钮后 Counter 组件是否正确更新计数。好了,上面我们测试了 React 基础组件。接下来,再来测试自定义 Hooks。
首先,我们先编写一个自定义 Hooks,接着我们再使用 React Testing Library 对它进行测试。
下面这段代码,你看到的是我将前面计算器的逻辑提取到一个名为 useCounter
的自定义钩子中:
// useCounter.tsx
import { useState } from "react";
type UseCounterProps = {
initialCount?: number
}
export const useCounter = ({ initialCount = 0 }: CounterProps = {}) => {
const [count, setCount] = useState(initialCount);
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
return { count, increment };
};
接着,让我们来探索一下如何使用 React Testing Library
对它进行测试:
// useCounter.test.tsx
import { render } from "@testing-library/react";
import { useCounter } from "./useCounter";
describe("useCounter", () => {
test("should render the initial count", () => {
render(useCounter) // error
});
})
这时候你会发现上面这段代码在执行的时候会有一些问题,在下面的内容中我会进行阐述。
测试自定义钩子不同于测试组件。当你尝试将钩子传递给 render()
函数来测试钩子时,你将收到一个类型错误,指示该钩子不能分配给 ReactElement<any, string | JSXElementConstructor<any>>
类型的参数。这是因为自定义钩子不返回任何JSX,这与 React 组件是不同的。
另一方面,如果你试图在不使用 render()
函数的情况下调用自定义 hooks
,也会在终端中看到错误,终端会指出 hooks
只能在函数组件中调用:
这么看来,测试自定义钩子确实有些棘手。
不过,别灰心,我的解决办法马上就要来了!
要在 React 中测试自定义钩子,我们可以使用 React Testing Library 测试库提供的 renderHook()
函数。这个函数允许我们渲染一个钩子并访问它的返回值。
接下来,在下面的代码中,让我们看看如何使用 renderHook()
重写 useCounter()
钩子的测试用例:
// useCounter.test.tsx
import { renderHook } from "@testing-library/react";
import { useCounter } from "./useCounter";
describe("useCounter", () => {
test("should render the initial count", () => {
const { result } = renderHook(useCounter);
expect(result.current.count).toBe(0);
});
})
在这个测试中,我们使用 renderHook()
来渲染 useCounter()
钩子,并使用 result
对象获得它的返回值。然后使用 expect()
去验证初始计数是否为 0。
需要注意的是,该值保存在 result.current
中。
同时,我们也可以通过传递一个 options
对象作为 renderHook()
的第二个参数来测试钩子是否接受并渲染相同的初始计数:
test("should accept and render the same initial count", () => {
const { result } = renderHook(useCounter, {
initialProps: { initialCount: 10 },
});
expect(result.current.count).toBe(10);
});
在这个测试中,我们使用 renderHook()
函数的 initialProps
选项将一个 initialCount
属性设置为 10 的 options
对象传递给我们的 useCounter()
钩子。然后使用 expect()
验证计数是否等于 10
。
接下来,让我们来看看如何测试事件。
为了测试 useCounter()
钩子的 increment
按钮功能是否如预期的那样工作,我们可以使用 renderHook()
来渲染钩子并调用 result.current.increment()
方法。
然而,当我们运行测试时,失败了,并显示一条错误信息:
Expected: 1
Received: 0
test("should increment the count", () => {
const { result } = renderHook(useCounter);
result.current.increment();
expect(result.current.count).toBe(1);
});
不过,错误信息提供了一个线索,指明了哪里出了问题:“在测试中对 TestComponent
的更新没有封装在 act(…)
中。
这么说,我们应该把 increment
方法,包装在 act(…)
中。
在 React Testing Library 中,act()
辅助函数会确保对组件进行的所有更新是在做出断言之前都能得到充分的处理。特别是在测试涉及状态更新的代码时,必须用 act()
函数包装该代码。这有助于准确地模拟组件的行为,并确保测试反映出真实的场景。
因此,我们对测试代码进行如下更改:
// useCounter.test.tsx
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'
test("should increment the count", () => {
const { result } = renderHook(useCounter);
act(() => result.current.increment()); // + update code
expect(result.current.count).toBe(1);
});
通过用 act()
包装 increment()
函数,我们可以确保在执行断言之前应用对状态的任何修改。这种方法还有助于避免由于异步更新而产生的潜在错误。
至此,我们完成了对自定义 Hooks 的测试工作。
当使用 React Testing Library 测试自定义钩子时,我们使用 renderHook()
函数来渲染我们的自定义钩子,并验证它是否返回预期的值。如果我们的自定义钩子接受props
,我们可以使用 renderHook()
函数的 initialProps
选项传递它们。
此外,我们必须确保任何导致状态更新的代码都用 act()
辅助函数包装,以防止出现错误。