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

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请求:

步骤示例:

  1. 创建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 }));
  }),
);
  1. 在测试文件中启用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 17:16:12