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

Vitest vi.spyOn测试React组件无效果,如何正确监听?

问题:vi.spyOn监听自定义Hook无效,组件仍使用原始数据

我在测试React组件时,尝试用vi.spyOnmock自定义Hook,但监听未生效,组件依旧运行原始逻辑。

测试代码

import * as useDatahooks from '../pages/TodoApp';
...
describe('Tasks', () => {
    const useDataSpy = vi.spyOn(useDatahooks, 'useData');

    it('should render the list of tasks', () => {
        const dataWant = [
            { id: "todo-0", name: "Eat", completed: true },
        ]
    
        useDataSpy.mockReturnValue({
            tasks: dataWant,
            setTasks: vi.fn(),
        });
    
        const { getByTestId } = render(<TodoApp />);
        expect(
            getByTestId('task-list').children.length
        ).toBe(dataWant.length);
    });
});

afterEach(() => {
    cleanup();
});

错误信息

AssertionError: expected 3 to be 1 // Object.is equality

组件及Hook代码

export interface TodoItem {
  id: string;
  name: string;
  completed: boolean;
}

const DATA :TodoItem[] = [
  { id: "todo-0", name: "Eat", completed: true },
  { id: "todo-1", name: "Sleep", completed: false },
  { id: "todo-2", name: "Repeat", completed: false },
];
export const useData = (data: TodoItem[]) => {
  const [tasks, setTasks] = useState<TodoItem[]>(data); // 仍使用原始数据而非Mock值
  return { tasks, setTasks };
}

export default function TodoApp() {
  const  { tasks, setTasks } = useData(DATA);
  ...
  const taskList = tasks?.map((task) => (
    <Todo
      id={task.id}
      name={task.name}
      completed={task.completed}
      key={task.id}
      deleteTask={deleteTask}
    />
  ));
  return (
      <ul
        data-testid="task-list"
        role="list"
        className="divide-y divide-gray-200 px-4 my-4"
        aria-labelledby="list-heading">
        {taskList}
      </ul>
    </div>
  )
}

原因分析

核心问题是模块内部引用未被监听:
TodoApp组件直接调用了同文件内定义的useData函数,而vi.spyOn只能监听模块导出的useData引用。组件实际执行的是未被mock的内部函数,所以mock不生效。


解决方法

方案1:将Hook拆分到独立文件

把useData单独抽离到独立文件,让组件和测试都从这个文件导入Hook,确保监听的是同一个引用:

  1. 创建hooks/useData.ts:
import { useState } from 'react';

export interface TodoItem {
  id: string;
  name: string;
  completed: boolean;
}

export const useData = (data: TodoItem[]) => {
  const [tasks, setTasks] = useState<TodoItem[]>(data);
  return { tasks, setTasks };
}
  1. 修改TodoApp组件导入:
import { useData, TodoItem } from '../hooks/useData';

const DATA :TodoItem[] = [
  { id: "todo-0", name: "Eat", completed: true },
  { id: "todo-1", name: "Sleep", completed: false },
  { id: "todo-2", name: "Repeat", completed: false },
];

export default function TodoApp() {
  const { tasks, setTasks } = useData(DATA);
  // ... 组件逻辑不变
}
  1. 测试文件更新导入路径:
import * as useDataHooks from '../hooks/useData';

describe('Tasks', () => {
    const useDataSpy = vi.spyOn(useDataHooks, 'useData');
    // ... 测试逻辑不变
});

方案2:修改组件内的Hook调用方式(不拆分文件)

如果不想拆分文件,让组件使用自身导出的useData引用,确保和测试监听的是同一个对象:

修改TodoApp组件:

import { useState } from 'react';

export interface TodoItem {
  id: string;
  name: string;
  completed: boolean;
}

export const DATA :TodoItem[] = [
  { id: "todo-0", name: "Eat", completed: true },
  { id: "todo-1", name: "Sleep", completed: false },
  { id: "todo-2", name: "Repeat", completed: false },
];
export const useData = (data: TodoItem[]) => {
  const [tasks, setTasks] = useState<TodoItem[]>(data);
  return { tasks, setTasks };
}

// 关键:导入自身导出的内容
import * as self from './TodoApp';

export default function TodoApp() {
  // 使用self.useData代替直接调用useData
  const { tasks, setTasks } = self.useData(self.DATA);
  // ... 组件逻辑不变
}

测试注意事项

每次测试后重置mock,避免影响其他测试用例:

afterEach(() => {
  vi.clearAllMocks(); // 重置mock状态
  cleanup();
});

内容的提问来源于stack exchange,提问作者Jenia Be Nice Please

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 15:23:12