You need to enable JavaScript to run this app. 如何测试调用了useReducer dispatch的自定义hook返回的函数
优惠活动
大模型
产品
解决方案
定价
更多

如何测试调用了useReducer dispatch的自定义hook返回的函数

自定义useApi Hook的测试方案

背景说明

我们在项目中封装了名为useApi的自定义Hook用于处理API调用,该Hook返回get、post、put、patch、del共5个函数,每个函数接收endpoint、success、fail三个参数,其中post、put、patch还额外接收body参数。

useApi核心实现代码

import React, { useContext, useState } from "react";
import * as api from "../../services/api";
import { GlobalContext } from "../../contexts/context";

export const useApi = () => {
    const context = useContext(GlobalContext);
    const [loading, setLoading] = useState(false);

    const get = async ({ endpoint, success, fail }) => {
        if (loading)
            return;

        try {
            setLoading(true);
            const result = await api.get("", endpoint, context);
            context.dispatch({ type: success, result });
            return result || {};
        } catch (err) {
            context.dispatch({ dispatch: fail, err });
            return {
                error: true,
                message: err,
            };
        } finally {
            setLoading(false);
        }
    };

    // 其他post/put/patch/del方法实现省略

    return {
        loading,
        get,
        post,
        put,
        patch,
        del,
    };
};

组件使用示例

该Hook的设计思路是在函数组件中调用它完成API交互,根据调用结果dispatch对应的action更新全局状态,组件使用示例如下:

const Example = () => {
    const { examples } = useContext(GlobalContext);
    const { loading, get } = useApi();

    const listExamples = async () => {
        await get({
            endpoint: '/example',
            success: 'LIST_EXAMPLES',
            fail: 'LIST_EXAMPLES_FAILED'
        });
    };

    useEffect(() => {
        listExamples();
    }, []);

    return (
        <Page
            title="Examples"
        >
            {
                !loading && examples.map(example => (<h1>{example}</h1>))

            }
            {
                loading &&
                <ProgressSpinner message={'Loading Workflows...'}/>
            }
        </Page>
    );
};

export default Example;

现有测试痛点

目前已经可以渲染组件并通过模拟数据校验列表内容符合预期,但存在两个核心问题:

  • 测试过程中仍然会发起真实的API请求
  • 不知道如何Mock才能校验自定义Hook返回的函数是否被触发,以及模拟函数调用时对应的全局状态变更

现有测试代码如下:

import React from 'react';
import { GlobalContext } from '../../contexts/context';
import init from '../../__tools__/contextSetup';
import ExampleList from '../../pages/Example/ExampleList/ExampleList';
import { render, screen, act } from '@testing-library/react';
import { ThemeProvider } from "@material-ui/core";
import { light } from "../../theme";

const example = {
    details: {
        description: 'Test Example 1 - Description',
        id: 'testingExample1',
        name: 'Test Example 1',
    },
    list: {
        data: [
            {
                description: 'Test Example 1 - Description',
                id: 'testingExample1',
                name: 'Test Example 1',
            },
        ],
        values: [
            {
                description: 'Test Example 1 - Description',
                id: 'testingExample1',
                name: 'Test Example 1',
            },
        ],
    }
};

describe('Example List', () => {
    it('loads the example list on mount', async () => {
        const dispatch = jest.fn();
        const state = { ...init };

        let ui;

        await act(async () => {
            ui = await render(
                <GlobalContext.Provider
                    value={{
                        ...state,
                        dispatch
                    }}
                >
                    <ThemeProvider theme={light}>
                        <ExampleList/>
                    </ThemeProvider>
                </GlobalContext.Provider>
            )
        });

        expect(await screen.getByText('There\'s nothing here...')).toBeInTheDocument();

        await (act(async () => {
            await ui.rerender(
                <GlobalContext.Provider
                    value={{
                        ...state,
                        dispatch,
                        example,
                    }}
                >
                    <ThemeProvider theme={light}>
                        <ExampleList/>
                    </ThemeProvider>
                </GlobalContext.Provider>
            )
        }));

        expect(await screen.getByText('Test Example 1')).toBeInTheDocument();
    });
})

解决方案

方案1:直接Mock useApi Hook(推荐,适合组件逻辑测试)

直接Mock整个useApi模块,既可以屏蔽真实API调用,也可以校验Hook方法的调用参数、模拟状态变更。

  1. 首先在测试文件顶部Mock useApi模块:
// 调整为你项目中useApi的实际路径
jest.mock('../../hooks/useApi.js', () => ({
  useApi: jest.fn()
}))
import { useApi } from '../../hooks/useApi.js'
  1. 在测试用例中注入Mock返回值,校验调用逻辑:
describe('Example List', () => {
    it('loads the example list on mount', async () => {
        const mockDispatch = jest.fn();
        const state = { ...init };

        // 构造Mock的get方法,模拟接口调用成功后的逻辑
        const mockGet = jest.fn(async ({ success }) => {
            mockDispatch({
                type: success,
                result: example.list.data
            })
        })
        // 给useApi注入返回值
        useApi.mockReturnValue({
            loading: false,
            get: mockGet,
            post: jest.fn(),
            put: jest.fn(),
            patch: jest.fn(),
            del: jest.fn()
        })

        await act(async () => {
            render(
                <GlobalContext.Provider
                    value={{
                        ...state,
                        dispatch: mockDispatch
                    }}
                >
                    <ThemeProvider theme={light}>
                        <ExampleList/>
                    </ThemeProvider>
                </GlobalContext.Provider>
            )
        });

        // 校验get方法被正确调用,参数符合预期
        expect(mockGet).toHaveBeenCalledWith({
            endpoint: '/example',
            success: 'LIST_EXAMPLES',
            fail: 'LIST_EXAMPLES_FAILED'
        })
        // 校验dispatch被触发,传入了正确的action
        expect(mockDispatch).toHaveBeenCalledWith({
            type: 'LIST_EXAMPLES',
            result: example.list.data
        })

        // 渲染包含数据的组件
        await act(async () => {
            await rerender(
                <GlobalContext.Provider
                    value={{
                        ...state,
                        dispatch: mockDispatch,
                        examples: example.list.data
                    }}
                >
                    <ThemeProvider theme={light}>
                        <ExampleList/>
                    </ThemeProvider>
                </GlobalContext.Provider>
            )
        });

        expect(await screen.getByText('Test Example 1')).toBeInTheDocument();
    });
})

方案2:Mock底层API服务(适合Hook本身逻辑测试)

如果需要测试useApi本身的逻辑,不需要Mock Hook,直接Mock底层的API服务模块即可屏蔽真实请求:

// 调整为你项目中api服务的实际路径
jest.mock('../../services/api', () => ({
  get: jest.fn(() => Promise.resolve(example.list.data)),
  post: jest.fn(() => Promise.resolve({})),
  // 其他方法按需Mock
}))

调用useApi的get方法时会自动走Mock的返回,不会发起真实网络请求,同时可以校验Hook内的dispatch逻辑是否符合预期。

内容的提问来源于stack exchange,提问作者Caleb Mills

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.10 15:42:02