NextJS + Jest:如何模拟服务端调用?
解决NextJS中Jest测试Server Actions + React Query的模拟难题
针对你遇到的场景,这里提供三个层级的解决方案,从最直接的模拟Server Actions到适配服务端请求的网络拦截,按需选择:
一、直接模拟Server Actions(最推荐)
既然实际数据请求是在Server Actions里执行的,跳过react-query的状态机,直接模拟Server Actions本身是最简洁的方式,完全避开react-query复杂的状态管理模拟。
步骤示例:
假设你的Server Actions定义在app/actions/data.ts中,包含查询用的fetchUser和修改用的updateUser:
// app/actions/data.ts 'use server'; export async function fetchUser(userId: string) { const res = await fetch(`https://your-api.com/users/${userId}`); return res.json(); } export async function updateUser(userId: string, data: { name: string }) { const res = await fetch(`https://your-api.com/users/${userId}`, { method: 'PUT', body: JSON.stringify(data), }); return res.json(); }
在测试文件中模拟这些Server Actions:
// 模拟整个Server Actions模块 jest.mock('../app/actions/data', () => ({ fetchUser: jest.fn(), updateUser: jest.fn(), })); import { fetchUser, updateUser } from '../app/actions/data'; import { render, screen, waitFor } from '@testing-library/react'; import UserProfile from '../app/components/UserProfile'; test('渲染用户数据', async () => { // 设置查询成功的返回值 (fetchUser as jest.Mock).mockResolvedValue({ id: '1', name: '测试用户' }); render(<UserProfile userId="1" />); // 断言数据渲染 await waitFor(() => expect(screen.getByText('测试用户')).toBeInTheDocument()); // 断言Server Action被正确调用 expect(fetchUser).toHaveBeenCalledWith('1'); }); test('更新用户名称', async () => { // 设置修改成功的返回值 (updateUser as jest.Mock).mockResolvedValue({ id: '1', name: '更新后的用户' }); render(<UserProfile userId="1" />); // 触发修改操作 await screen.getByRole('button', { name: '修改名称' }).click(); await screen.getByLabelText('新名称').type('更新后的用户'); await screen.getByRole('button', { name: '确认' }).click(); // 断言更新后的数据渲染 await waitFor(() => expect(screen.getByText('更新后的用户')).toBeInTheDocument()); // 断言修改接口被正确调用 expect(updateUser).toHaveBeenCalledWith('1', { name: '更新后的用户' }); });
二、模拟React Query钩子(适合必须钩子层面模拟的场景)
如果因为某些原因需要直接模拟useServiceQuery或useQuery/useMutation,可以借助React Query官方的测试工具,或者手动模拟状态:
用官方测试工具简化状态模拟
安装@tanstack/react-query/testing后,使用mockQueryClient来快速设置查询/修改的状态:
import { render, screen, waitFor } from '@testing-library/react'; import { QueryClientProvider } from '@tanstack/react-query'; import { mockQueryClient } from '@tanstack/react-query/testing'; import UserProfile from '../app/components/UserProfile'; test('渲染加载状态和成功数据', async () => { const queryClient = mockQueryClient(); render( <QueryClientProvider client={queryClient}> <UserProfile userId="1" /> </QueryClientProvider> ); // 断言加载状态显示 expect(screen.getByText('加载中...')).toBeInTheDocument(); // 设置查询成功的返回值 queryClient.setQueryData(['user', '1'], { id: '1', name: '测试用户' }); // 断言成功数据渲染 await waitFor(() => expect(screen.getByText('测试用户')).toBeInTheDocument()); });
手动模拟useMutation的mutate函数
如果需要模拟useMutation的mutate/mutateAsync,可以直接mock钩子的返回值:
jest.mock('@tanstack/react-query', () => ({ ...jest.requireActual('@tanstack/react-query'), useMutation: jest.fn(() => ({ mutateAsync: jest.fn().mockResolvedValue({ id: '1', name: '更新后的用户' }), isLoading: false, error: null, })), })); // 后续测试中可以直接触发mutate,断言状态变化
三、用MSW拦截服务端请求(适合不想修改业务代码的场景)
因为Server Actions在Node环境执行,MSW支持Node端的请求拦截,不需要模拟任何函数,直接拦截真实的API请求:
步骤示例:
- 创建MSW服务端配置:
// mocks/server.ts import { setupServer } from 'msw/node'; import { rest } from 'msw'; export const server = setupServer( // 拦截用户查询请求 rest.get('https://your-api.com/users/:userId', (req, res, ctx) => { return res(ctx.json({ id: req.params.userId, name: 'MSW模拟用户' })); }), // 拦截用户修改请求 rest.put('https://your-api.com/users/:userId', (req, res, ctx) => { return res(ctx.json({ id: req.params.userId, name: req.body.name })); }), );
- 在测试文件中启用MSW:
import { server } from '../mocks/server'; import { render, screen, waitFor } from '@testing-library/react'; import UserProfile from '../app/components/UserProfile'; // 测试前启动服务 beforeAll(() => server.listen()); // 每个测试后重置拦截规则 afterEach(() => server.resetHandlers()); // 测试结束后关闭服务 afterAll(() => server.close()); test('MSW拦截查询请求', async () => { render(<UserProfile userId="1" />); await waitFor(() => expect(screen.getByText('MSW模拟用户')).toBeInTheDocument()); }); test('MSW拦截修改请求', async () => { render(<UserProfile userId="1" />); await screen.getByRole('button', { name: '修改名称' }).click(); await screen.getByLabelText('新名称').type('MSW更新用户'); await screen.getByRole('button', { name: '确认' }).click(); await waitFor(() => expect(screen.getByText('MSW更新用户')).toBeInTheDocument()); });
内容的提问来源于stack exchange,提问作者darko
相关产品推荐
相关产品推荐

