You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js项目中使用jest.spyOn无法Mock自定义Hook的问题

动态Mock自定义Hook并修改返回值的解决方案(Next.js 12.3.0)

问题背景

在Next.js 12.3.0项目中,需要Mock自定义Hook并在单个测试用例中动态修改其返回值,当前遇到以下问题:

  • 直接使用jest.spyOn监听导出的Hook模块时,报错TypeError: Cannot redefine property: useMyHook
  • 使用jest.mock虽能Mock成功,但无法在单个测试用例中动态调整返回值
  • 现有mock-hooks.js中的Mock逻辑不生效,测试仍输出原始Hook的返回值

现有代码

useMyHook.jsx

const useMyHook = () => {
  const [val, setVal] = useState(200)

  return { val }
}

export { useMyHook }

Hello.jsx

import { useTestHook } from "@/hooks/useTestHook";

export const Hello = () => {
  const { val } = useTestHook();
  console.log({ val });

  return (
    <div>
      <h2>Hello</h2>
    </div>
  );
};

mock-hooks.js

import { useMyHook } from "@/hooks/useMyHook";
// import * as Hook from "@/hooks/useMyHook";

const mocks = {
  useMyHook: (val = 9999) => {
    jest
      .spyOn({ useMyHook }, "useMyHook")
      .mockImplementation(() => ({ val }));

    /** 此Mock方法之前可用,但现在报错 `TypeError: Cannot redefine property: useMyHook` */
    // jest.spyOn(Hook, "useMyHook")
    //   .mockImplementation(() => ({ val }));

  },
};

export { mocks };

Hello.test.jsx

import { mocks } from "@/utils/test/mock-hooks";

import { Hello } from "./Hello";
import { render } from "@testing-library/react";

describe("Hello test", () => {
  beforeEach(() => {
    mocks.useMyHook(12345);
    render(<Hello />, {
      wrapper: ({ children }) => <>{children}</>,
    });
  });

  test("should render the main container", () => {
    screen.debug()
  });
});

项目依赖

{
  ...
 "dependencies": {
    "next": "12.3.0",
    "react": "17.0.2",
    "react-dom": "17.0.2"
  },
  "devDependencies": {
    "@babel/core": "7.17.8",
    "@testing-library/jest-dom": "5.16.4",
    "@testing-library/react": "12.1.2",
    "@testing-library/react-hooks": "7.0.2",
    "babel-jest": "27.4.6",
    "jest": "28.1.0",
    "jest-environment-jsdom": "28.1.0"
  }
}

jest.config.js

const nextJest = require("next/jest");

const createJestConfig = nextJest({});

const customJestConfig = {
  collectCoverageFrom: ["**/*.{js,jsx,ts,tsx}"],
  verbose: true,
  modulePaths: ["<rootDir>/"],
  modulePathIgnorePatterns: ["<rootDir>/.next"],
  testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/.next/"],
  transform: {
    "^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }],
  },
  transformIgnorePatterns: [
    "/node_modules/",
    "^.+\\.module\\.(css|sass|scss)$",
  ],
  testEnvironment: "jsdom",
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/$1",
  },
};

module.exports = createJestConfig(customJestConfig);

.babelrc

{
  "presets": ["next/babel"],
  "plugins": []
}

可行解决方案

方案1:jest.mock配合可动态修改的Mock函数

通过全局Mock整个Hook模块,再修改Mock函数的实现来动态调整返回值:

修改mock-hooks.js

import * as HookModule from "@/hooks/useMyHook";

// 全局Mock整个模块,默认返回原始值
jest.mock("@/hooks/useMyHook", () => ({
  useMyHook: jest.fn(() => ({ val: 200 }))
}));

const mocks = {
  useMyHook: (val = 9999) => {
    // 动态更新Mock函数的返回值
    HookModule.useMyHook.mockImplementation(() => ({ val }));
  },
  // 重置Mock,避免用例间状态污染
  resetAllMocks: () => {
    jest.clearAllMocks();
    HookModule.useMyHook.mockImplementation(() => ({ val: 200 }));
  }
};

export { mocks };

修改Hello.test.jsx

import { mocks } from "@/utils/test/mock-hooks";
import { Hello } from "./Hello";
import { render, screen } from "@testing-library/react";

describe("Hello test", () => {
  beforeEach(() => {
    mocks.resetAllMocks(); // 每个用例前重置Mock
  });

  test("render with default mock value", () => {
    render(<Hello />);
    // 此时val为默认的200
  });

  test("render with custom value 12345", () => {
    mocks.useMyHook(12345); // 先设置自定义返回值,再渲染组件
    render(<Hello />);
    // 此时val为12345
  });
});

方案2:绕过模块冻结问题

Next.js 12会冻结顶层模块导出,导致直接spyOn单个Hook报错。可以通过统一导出层规避:

1. 新增统一导出文件

在hooks目录下创建index.js:

export { useMyHook } from "./useMyHook";

2. 修改Hello组件的导入路径

import { useMyHook } from "@/hooks"; // 替换原@/hooks/useMyHook

3. 修改mock-hooks.js

import * as HookModule from "@/hooks";

const mocks = {
  useMyHook: (val = 9999) => {
    jest.spyOn(HookModule, "useMyHook")
      .mockImplementation(() => ({ val }));
  },
  // 恢复原始Hook实现
  resetUseMyHook: () => {
    HookModule.useMyHook.mockRestore();
  }
};

export { mocks };

关键注意事项

  • 必须在渲染组件前调用Mock设置方法,否则组件会使用原始Hook的返回值
  • 每个测试用例结束后要重置Mock,避免用例间状态污染,可使用mockRestore()或clearAllMocks()
  • Next.js 12的模块冻结机制会导致直接spyOn单个导出Hook失败,需通过统一导出层或全局Mock规避

内容的提问来源于stack exchange,提问作者javascript-wtf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 13:17:15