React不使用自定义Hook如何Mock测试组件localStorage逻辑
答案
你完全不需要额外编写自定义Hook,也不是必须用renderHook才能测试localStorage相关逻辑,直接Mock全局localStorage API,通过常规的组件渲染+用户交互模拟就能覆盖所有存取场景。
具体测试实现(以React Testing Library + Vitest/Jest为例)
前置Mock配置
在每个测试用例执行前重写localStorage的原生方法,避免用例间数据污染:
import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, it, expect, beforeEach, vi } from 'vitest'; // 用Jest的话把vi替换成jest即可 import TodoList from './TodoList'; beforeEach(() => { // Mock localStorage的两个核心方法 Storage.prototype.getItem = vi.fn(); Storage.prototype.setItem = vi.fn(); // 清空所有Mock的调用记录 vi.clearAllMocks(); });
测试用例1:组件初始化读取localStorage缓存
it('组件挂载时正确读取localStorage中已存储的待办列表', () => { // 预设localStorage里的存储内容 const mockStoredTasks = JSON.stringify([ { taskName: '写单元测试', id: 1710000000000 } ]); // 让getItem调用返回预设值 (Storage.prototype.getItem as ReturnType<typeof vi.fn>).mockReturnValue(mockStoredTasks); render(<TodoList />); // 断言页面渲染了缓存的待办项 expect(screen.getByText('写单元测试')).toBeInTheDocument(); // 断言确实调用了localStorage读取,且key正确 expect(Storage.prototype.getItem).toHaveBeenCalledWith('localTasks'); });
测试用例2:新增待办时写入localStorage
it('添加新待办后,将最新列表同步写入localStorage', async () => { const user = userEvent.setup(); // 初始化时localStorage无数据 (Storage.prototype.getItem as ReturnType<typeof vi.fn>).mockReturnValue(null); render(<TodoList />); // 模拟用户输入待办内容、点击添加按钮 await user.type(screen.getByPlaceholderText('enter a task'), '买牛奶'); await user.click(screen.getByTestId('add-todo')); // 断言localStorage写入方法被调用,key正确,内容包含新增的待办 expect(Storage.prototype.setItem).toHaveBeenCalledWith( 'localTasks', expect.stringContaining('买牛奶') ); // 断言输入框提交后被清空 expect(screen.getByPlaceholderText('enter a task')).toHaveValue(''); });
测试用例3:删除待办时更新localStorage
it('删除待办后,同步更新localStorage中的列表', async () => { const user = userEvent.setup(); const testTaskId = 1710000000000; // 预设localStorage里有一条待办 const mockStoredTasks = JSON.stringify([ { taskName: '要删除的待办', id: testTaskId } ]); (Storage.prototype.getItem as ReturnType<typeof vi.fn>).mockReturnValue(mockStoredTasks); render(<TodoList />); // 点击对应待办的删除按钮 await user.click(screen.getByTestId(`delete-todo-${testTaskId}`)); // 断言写入localStorage的是删除后的空列表 expect(Storage.prototype.setItem).toHaveBeenCalledWith( 'localTasks', JSON.stringify([]) ); });
额外代码问题提醒
你更新版代码里的删除逻辑有bug:当前过滤条件写的是index !== todo.id,但id是时间戳,和数组索引没有对应关系,会出现删错项的问题,修正如下:
// 原错误逻辑 // const handleDelete = () => { // setTodoList((prevTodoList) => prevTodoList.filter((todo, index) => index !== todo.id)); // }; // 修正后 const handleDelete = (taskId: number) => { setTodoList((prevTodoList) => prevTodoList.filter((todo) => todo.id !== taskId)); }; // 按钮调用时记得传入id:onClick={() => handleDelete(todo.id)}
另外补充:是否抽取自定义Hook完全取决于你要不要复用这段localStorage存取逻辑,和测试没有关系,不需要为了测试特意抽离Hook,当前内联在组件里的逻辑完全可以通过上述方式完整覆盖。
内容的提问来源于stack exchange,提问作者goatstash
相关产品推荐
相关产品推荐

