Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性

测试 React Hooks

在 React 开发中,Hooks 是一个非常重要的功能模块,允许开发者在函数组件中使用状态和其他 React 特性。自定义 Hooks 作为一种公共逻辑的抽离,经常被多个组件复用,因此对其测试是非常必要的。

然而,由于 Hooks 必须在组件内部使用,直接测试它们并不像普通函数那样简单。幸运的是,@testing-library/react-hooks 提供了一种简便的方法来测试 React Hooks。

安装和使用 @testing-library/react-hooks

首先,确保你已经安装了 @testing-library/react-hooks

sh 复制代码
npm install @testing-library/react-hooks
快速上手示例

假设我们有一个自定义 Hook useCounter,用于创建一个计数器:

ts 复制代码
// 自定义 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 进行测试。

测试同步操作
ts 复制代码
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:

ts 复制代码
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 中添加了一个异步的增加方法:

ts 复制代码
const asyncInc = (delta = 1) => {
  setTimeout(() => {
    setValue((c) => c + delta);
  }, 2000);
};

测试异步操作时,可以使用 jestuseFakeTimersadvanceTimersByTime 方法来模拟时间流逝:

ts 复制代码
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 的行为进行测试,可以确保它们在不同情况下的表现符合预期,从而提高代码的可靠性和可维护性。

相关推荐
Boilermaker19921 小时前
【Java EE】SpringIoC
前端·数据库·spring
中微子1 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y2 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁2 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry2 小时前
Fetch 笔记
前端·javascript
拾光拾趣录2 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟2 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan2 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson2 小时前
青苔漫染待客迟
前端·设计模式·架构