如何测试调用了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方法的调用参数、模拟状态变更。
- 首先在测试文件顶部Mock useApi模块:
// 调整为你项目中useApi的实际路径 jest.mock('../../hooks/useApi.js', () => ({ useApi: jest.fn() })) import { useApi } from '../../hooks/useApi.js'
- 在测试用例中注入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
相关产品推荐
相关产品推荐

