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

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 });
    })
})

问题原因与修复方案

核心问题

  1. 状态引用未更新:测试中一开始解构的apiState是初始状态的快照,后续状态更新后这个变量不会自动同步最新值,必须每次从result.current中重新获取。
  2. 异步函数未正确包裹:fireAPI是异步函数,同步的act无法捕获其内部的状态更新,需要使用await act(async () => { ... })包裹异步操作。
  3. 等待方式不可靠:自定义的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 04:20:57