测试 React Hooks
在 React 开发中,Hooks 是一个非常重要的功能模块,允许开发者在函数组件中使用状态和其他 React 特性。自定义 Hooks 作为一种公共逻辑的抽离,经常被多个组件复用,因此对其测试是非常必要的。
然而,由于 Hooks 必须在组件内部使用,直接测试它们并不像普通函数那样简单。幸运的是,@testing-library/react-hooks
提供了一种简便的方法来测试 React Hooks。
安装和使用 @testing-library/react-hooks
首先,确保你已经安装了 @testing-library/react-hooks
:
npm install @testing-library/react-hooks
快速上手示例
假设我们有一个自定义 Hook useCounter
,用于创建一个计数器:
// 自定义 hook
// 这是一个计数器的自定义 hook
// 内部维护了一个计数的值,以及修改这个值的一些方法import { useState } from "react";interface Options {min?: number;max?: number;
}type ValueParam = number | ((c: number) => number);// 该方法主要是做一个边界的判断,如果超过了边界,那么就取边界值
function getTargetValue(val: number, options: Options = {}) {const { min, max } = options;let target = val;// 判断有没有超过最大值,如果超过了,那么我们就取最大值if (typeof max === "number") {target = Math.min(max, target);}// 判断有没有超过最小值,如果超过了,那么我们就取最小值if (typeof min === "number") {target = Math.max(min, target);}return target;
}// useCounter(100, {min : 1, max : 1000})
function useCounter(initialValue = 0, options: Options = {}) {const { min, max } = options;// 设置初始值,初始值就为 initialVaule// 初始值是该自定义 hook 内部维护的状态,用来表示计数器的数值const [current, setCurrent] = useState(() => {return getTargetValue(initialValue, {min,max,});});// 设置新的值// 在设置新的值的时候,调用了 getTargetValue 来判断新值是否越界const setValue = (value: ValueParam) => {setCurrent((c) => {const target = typeof value === "number" ? value : value(c);return getTargetValue(target, {max,min,});});};// 下面就是自定义 hook 提供的 4 个方法// 用于修改计数器的数值// 增加const inc = (delta = 1) => {setValue((c) => c + delta);};// 减少const dec = (delta = 1) => {setValue((c) => c - delta);};// 设置const set = (value: ValueParam) => {setValue(value);};// 重置const reset = () => {setValue(initialValue);};// 像外部暴露return [current,{inc,dec,set,reset,},] as const;
}export default useCounter;
接下来,我们将对这个自定义 Hook 进行测试。
测试同步操作
import useCounter from "../hooks/useCounter";
import { renderHook, act } from "@testing-library/react";test("可以做加法", () => {// Arrange(准备)// result ---> {current : [0, {inc, dec, set, reset}]}const { result } = renderHook(() => useCounter(0));// Act(行为)act(() => result.current[1].inc(2));// Assert(断言)expect(result.current[0]).toEqual(2);
});test("可以做减法", () => {// Arrange(准备)// result ---> {current : [0, {inc, dec, set, reset}]}const { result } = renderHook(() => useCounter(0));// Act(行为)act(() => result.current[1].dec(2));// Assert(断言)expect(result.current[0]).toEqual(-2);
});test("可以设置值", () => {// Arrange(准备)// result ---> {current : [0, {inc, dec, set, reset}]}const { result } = renderHook(() => useCounter(0));// Act(行为)act(() => result.current[1].set(100));// Assert(断言)expect(result.current[0]).toEqual(100);
});test("可以重置值", () => {// Arrange(准备)// result ---> {current : [0, {inc, dec, set, reset}]}const { result } = renderHook(() => useCounter(0));// Act(行为)act(() => result.current[1].set(100));act(() => result.current[1].reset());// Assert(断言)expect(result.current[0]).toEqual(0);
});test("可以设置最大值", () => {// Arrange(准备)// result ---> {current : [0, {inc, dec, set, reset}]}const { result } = renderHook(() => useCounter(0, { max: 100 }));// Act(行为)act(() => result.current[1].set(1000));// Assert(断言)expect(result.current[0]).toEqual(100);
});test("可以设置最小值", () => {// Arrange(准备)// result ---> {current : [0, {inc, dec, set, reset}]}const { result } = renderHook(() => useCounter(0, { min: -100 }));// Act(行为)act(() => result.current[1].set(-1000));// Assert(断言)expect(result.current[0]).toEqual(-100);
});
使用自定义 Hook
测试通过后,可以在组件中安全地使用这个自定义 Hook:
import "./App.css";
import useCounter from "./hooks/useCounter";function App() {const [current, { inc, dec, set, reset }] = useCounter(5, { min: 0, max: 10 });return (<div className="App"><div>{current}</div><button onClick={() => dec(1)}>-</button><button onClick={() => inc(1)}>+</button><button onClick={() => set(100)}>set</button><button onClick={() => reset()}>reset</button></div>);
}export default App;
测试异步操作
假设我们在 useCounter
中添加了一个异步的增加方法:
const asyncInc = (delta = 1) => {setTimeout(() => {setValue((c) => c + delta);}, 2000);
};
测试异步操作时,可以使用 jest
的 useFakeTimers
和 advanceTimersByTime
方法来模拟时间流逝:
test("测试异步的增加", async () => {jest.useFakeTimers();const { result } = renderHook(() => useCounter(0));act(() => result.current[1].asyncInc(2));expect(result.current[0]).toEqual(0); // 初始值未变await act(() => jest.advanceTimersByTime(2000)); // 模拟时间流逝expect(result.current[0]).toEqual(2); // 值已更新jest.useRealTimers();
});
结论
通过本文的介绍,我们了解了如何使用 @testing-library/react-hooks
测试 React Hooks,特别是自定义 Hooks。通过对 Hooks 的行为进行测试,可以确保它们在不同情况下的表现符合预期,从而提高代码的可靠性和可维护性。