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,确保监听的是同一个引用:
- 创建
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 }; }
- 修改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); // ... 组件逻辑不变 }
- 测试文件更新导入路径:
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
相关产品推荐
相关产品推荐

