React自定义Fetch Hook测试用例失败,状态未更新问题排查
React自定义Fetch Hook测试失败排查与修复
我开发了一个用于执行Fetch请求的React自定义Hook,编写测试用例后发现运行失败,核心问题是setApiState的状态更新没有在测试中被正确捕获。尝试过在act中使用await以及waitFor方法,但问题仍未解决。
自定义Hook代码
import { useState } from 'react'; export interface IFetchHook { path: string; options?: Record<string, any>; resHandler?: (param: any) => void; errorHandler?: (obj: Record<string, any>) => void; } const getOptions = (params: any) => { const defaults = { method: 'GET', headers: { 'Content-Type': 'application/json' } }; const newObj = { ...defaults, ...params }; // header是对象,其他字段为普通值,递归合并 if (params?.headers) { newObj.headers = { ...defaults.headers, ...params.headers }; } return newObj; }; const useFetchHook = ({ path = '', options = {}, resHandler, errorHandler }: IFetchHook) => { const [apiState, setApiState] = useState({ loading: false, data: null, error: null }); const fetchOptions = getOptions(options); const fireAPI = async () => { if (typeof fetch == 'undefined') { throw new Error('fetch not defined'); } try { setApiState({ loading: true, data: null, error: null }); const res = await fetch(path, fetchOptions); if (resHandler) { resHandler(res); setApiState({ loading: false, data: null, error: null }); } else { const parsedRes = await res.json(); setApiState({ loading: false, data: parsedRes, error: null }); } } catch (error: any) { setApiState({ loading: false, data: null, error }); if (errorHandler) { errorHandler({ path, fetchOptions, error }); } } } return [fireAPI, apiState]; } export default useFetchHook;
原测试用例代码
// 导入所需模块 import { act, renderHook } from '@testing-library/react-hooks'; import useFetchHook from './index'; // Mock fetch函数 global.fetch = jest.fn(); // 定义测试数据 const mockTodo = { id: 1, title: 'Test todo', completed: false }; const mockPath = 'https://jsonplaceholder.typicode.com/todos/1'; const mockOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' } }; // 定义等待下一个tick的辅助函数 const waitForNextTick = () => new Promise((resolve) => setTimeout(resolve)); // 定义mock响应处理函数 const mockResHandler = jest.fn(); // 定义mock错误处理函数 const mockErrorHandler = jest.fn(); describe('useFetchHook', () => { let originalFetch: any; beforeEach(() => { originalFetch = global.fetch; }); afterEach(() => { global.fetch = originalFetch; }); beforeEach(() => { mockResHandler.mockClear(); mockErrorHandler.mockClear(); }); // 测试成功请求场景 it.only('应该获取数据并更新状态', async () => { (fetch as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => mockTodo }); const { result } = renderHook(() => useFetchHook({ path: mockPath, options: mockOptions })); const [fireAPI, apiState] = result.current as [() => Promise<void>, { loading: boolean; data: null | unknown, error: null | unknown }]; act(() => { fireAPI(); }); expect(apiState).toEqual({ loading: true, data: null, error: null }); await waitForNextTick(); expect(apiState).toEqual({ loading: false, data: mockTodo, error: null }); }) })
问题原因与修复方案
核心问题
- 状态引用未更新:测试中一开始解构的
apiState是初始状态的快照,后续状态更新后这个变量不会自动同步最新值,必须每次从result.current中重新获取。 - 异步函数未正确包裹:
fireAPI是异步函数,同步的act无法捕获其内部的状态更新,需要使用await act(async () => { ... })包裹异步操作。 - 等待方式不可靠:自定义的
waitForNextTick依赖setTimeout,不如@testing-library/react-hooks提供的waitFor可靠,后者会自动等待状态更新完成。
修改后的测试用例
// 导入所需模块 import { act, renderHook, waitFor } from '@testing-library/react-hooks'; import useFetchHook from './index'; // Mock fetch函数 global.fetch = jest.fn(); // 定义测试数据 const mockTodo = { id: 1, title: 'Test todo', completed: false }; const mockPath = 'https://jsonplaceholder.typicode.com/todos/1'; const mockOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' } }; // 定义mock响应处理函数 const mockResHandler = jest.fn(); // 定义mock错误处理函数 const mockErrorHandler = jest.fn(); describe('useFetchHook', () => { let originalFetch: any; beforeEach(() => { originalFetch = global.fetch; }); afterEach(() => { global.fetch = originalFetch; jest.clearAllMocks(); }); // 测试成功请求场景 it('应该获取数据并更新状态', async () => { (fetch as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => mockTodo }); const { result } = renderHook(() => useFetchHook({ path: mockPath, options: mockOptions })); // 调用fireAPI并等待异步操作完成 await act(async () => { await result.current[0](); }); // 断言最终状态 await waitFor(() => { expect(result.current[1]).toEqual({ loading: false, data: mockTodo, error: null }); }); // 可选:分步断言加载状态 // await act(async () => { // result.current[0](); // }); // expect(result.current[1]).toEqual({ loading: true, data: null, error: null }); // await waitFor(() => { // expect(result.current[1]).toEqual({ loading: false, data: mockTodo, error: null }); // }); }) })
额外优化点
- 移除自定义的
waitForNextTick,直接使用waitFor - 在
afterEach中调用jest.clearAllMocks()确保mock状态干净 - 测试完成后移除
it.only,改为普通it
内容的提问来源于stack exchange,提问作者x Tribe
相关产品推荐
相关产品推荐

