如何使用Jest为自定义useLocalStorage Hook编写单元测试
Jest 测试自定义Hook
useLocalStorage 实操方案 前置准备
这个Hook是React自定义Hook,不能直接调用测试,需要搭配@testing-library/react提供的renderHook工具挂载测试。Jest默认带的jsdom环境已经内置了localStorage的mock实现,不需要额外引入第三方mock库,只需要在每个用例执行前清空localStorage,避免不同用例的缓存互相干扰即可。
必须覆盖的测试场景
- SSR场景(
window对象不存在)下初始化,直接返回传入的initialValue,不会调用localStorage相关方法 - 浏览器环境下,localStorage中没有对应key的缓存时,初始化返回
initialValue - 浏览器环境下,localStorage中存在对应key的合法缓存时,初始化正确反序列化JSON返回存储值
- 读取localStorage抛出异常时(比如存储内容是非法JSON、localStorage被浏览器权限禁用),能捕获错误打印日志,正常返回
initialValue不崩溃 - 调用
setValue传入普通值时,能正确更新内部state,同时将值序列化后写入localStorage对应key - 调用
setValue传入更新函数时,能基于当前存储值计算新值,同步更新state和localStorage - 写入localStorage抛出异常时(比如存储配额满、无写入权限),能捕获错误打印日志,不阻塞代码执行
- SSR场景下调用
setValue,只会更新内部state,不会尝试访问localStorage的setItem方法
完整测试代码
首先在测试文件顶部配置测试环境,写通用的重置逻辑:
import { renderHook, act } from '@testing-library/react'; // 替换成你项目中useLocalStorage文件的实际引入路径 import { useLocalStorage } from './useLocalStorage'; beforeEach(() => { // 每个用例执行前清空localStorage window.localStorage.clear(); // mock console.log 避免错误信息污染测试输出 jest.spyOn(console, 'log').mockImplementation(() => {}); }); afterEach(() => { // 每个用例执行后还原所有mock jest.restoreAllMocks(); });
逐个场景写测试用例:
test('SSR环境window不存在时,初始化返回初始值', () => { const originalWindow = global.window; // 模拟SSR下window被销毁的场景 delete global.window; const { result } = renderHook(() => useLocalStorage('testKey', 'defaultValue')); const [storedValue] = result.current; expect(storedValue).toBe('defaultValue'); // 还原window对象避免影响其他用例 global.window = originalWindow; }); test('localStorage无对应缓存时,初始化返回传入的初始值', () => { const { result } = renderHook(() => useLocalStorage('testKey', 1234)); const [storedValue] = result.current; expect(storedValue).toBe(1234); }); test('localStorage有对应合法缓存时,初始化正确解析返回存储值', () => { const cacheValue = { username: 'test', age: 20 }; window.localStorage.setItem('testKey', JSON.stringify(cacheValue)); const { result } = renderHook(() => useLocalStorage('testKey', {})); const [storedValue] = result.current; expect(storedValue).toEqual(cacheValue); }); test('读取localStorage抛错时,捕获错误返回初始值', () => { // mock getItem抛错模拟异常场景 jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('localStorage is disabled'); }); const { result } = renderHook(() => useLocalStorage('testKey', 'fallbackVal')); const [storedValue] = result.current; expect(storedValue).toBe('fallbackVal'); expect(console.log).toHaveBeenCalledWith(expect.any(Error)); }); test('调用setValue传入普通值时,同步更新state和localStorage', () => { const { result } = renderHook(() => useLocalStorage('testKey', 'initVal')); const [, setValue] = result.current; // state更新必须包裹在act中,符合React的渲染规则 act(() => { setValue('newVal'); }); const [updatedVal] = result.current; expect(updatedVal).toBe('newVal'); expect(JSON.parse(window.localStorage.getItem('testKey'))).toBe('newVal'); }); test('调用setValue传入更新函数时,基于当前值计算新值并存储', () => { const { result } = renderHook(() => useLocalStorage('testKey', 0)); const [, setValue] = result.current; act(() => { setValue(prev => prev + 10); }); const [updatedVal] = result.current; expect(updatedVal).toBe(10); expect(JSON.parse(window.localStorage.getItem('testKey'))).toBe(10); }); test('写入localStorage抛错时,捕获错误不导致代码崩溃', () => { // mock setItem抛错模拟写入异常 jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('storage quota exceeded'); }); const { result } = renderHook(() => useLocalStorage('testKey', 'initVal')); const [, setValue] = result.current; expect(() => { act(() => { setValue('writeFailVal'); }); }).not.toThrow(); expect(console.log).toHaveBeenCalledWith(expect.any(Error)); });
注意:所有触发React state更新的操作(比如这里的
setValue调用)必须包裹在act()函数内,否则会收到React的测试警告,且拿到的state值可能不是更新后的最新值。
内容的提问来源于stack exchange,提问作者user19521773
相关产品推荐
相关产品推荐

