在 Next.js 中测试 React Server Components

发布: (2026年2月28日 GMT+8 14:00)
9 分钟阅读
原文: Dev.to

Source: Dev.to

(请提供您希望翻译的正文内容,我将为您翻译成简体中文,并保留原始的格式、代码块和链接。)

理解 React Server Components

在编写测试之前,先了解一下 RSC 与客户端组件的区别会很有帮助:

  • 没有客户端状态或生命周期方法。 RSC 不能使用 useStateuseEffect 或任何依赖浏览器环境的 Hook。
  • 直接访问服务器端数据。 它们可以在顶层 await 数据库查询、文件读取或 fetch 调用——无需基于 useEffect 的数据获取。
  • 零客户端 JavaScript 输出。 组件的逻辑保留在服务器上;仅将生成的 HTML 发送给浏览器。

在 Next.js 的 app 目录中,除非显式包含 'use client' 指令,否则组件默认是服务器组件。这意味着你的大多数组件树——布局、页面以及大多数数据获取组件——都将是 RSC。

为什么测试 RSC 与众不同

标准的 React 测试工具(如 @testing-library/react)通过在模拟浏览器的 jsdom 环境中挂载组件来工作。RSC 并不在浏览器中运行,因此 jsdom 不是合适的环境。尝试使用标准的 render 函数渲染 RSC 要么会直接失败,要么会产生误导性的结果。

需要规划的具体挑战

  • 服务器上下文。 RSC 可能依赖仅在服务器上可用的 API——cookies()headers() 或直接的数据库访问——这些在没有显式 mock 的测试环境中是不存在的。
  • 异步渲染。 与类组件或基于 Hook 的组件不同,RSC 是返回 JSX 的 async 函数。这会影响你在测试中如何渲染和断言。
  • Next.js 特定 API。next/navigation 中的 redirect()notFound(),以及 next/headers 模块等函数,需要进行 mock,以避免测试时出现错误。

Source:

工具与设置

依赖

npm install --save-dev jest @testing-library/react @testing-library/jest-dom msw

对于 TypeScript 项目,还需安装 ts-jest@types/jest

Jest 配置

使用 node 测试环境,而不是 jsdom,因为 RSC 在服务器上运行:

// jest.config.js
module.exports = {
  testEnvironment: 'node',
  moduleNameMapper: {
    '^@/(.*)$': '/src/$1',
  },
  setupFilesAfterEnv: ['/jest.setup.js'],
};
// jest.setup.js
import '@testing-library/jest-dom';

Mocking Next.js Server APIs

需要对多个 Next.js 模块进行 mock,以防止测试失败。在项目根目录下创建 __mocks__ 目录:

// __mocks__/next/headers.js
export const cookies = jest.fn(() => ({
  get: jest.fn(),
  set: jest.fn(),
}));

export const headers = jest.fn(() => new Headers());
// __mocks__/next/navigation.js
export const redirect = jest.fn();
export const notFound = jest.fn();
export const useRouter = jest.fn(() => ({
  push: jest.fn(),
  replace: jest.fn(),
}));

为 RSC 编写测试

待测试的组件

下面是一个直接的 RSC 示例,它获取并显示用户列表:

// app/components/UserList.js
export default async function UserList() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await res.json();

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>- {user.name}</li>
      ))}
    </ul>
  );
}

在测试中渲染 RSC

由于 RSC 是 async 函数,需要在将结果传递给渲染器之前 await 它们。使用 react-dom/server 提供的 renderToString 来生成 HTML 输出:

import React from 'react';
import { renderToString } from 'react-dom/server';
import UserList from '@/components/UserList';

// Mock the global fetch
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () =>
      Promise.resolve([
        { id: 1, name: 'John Doe' },
        { id: 2, name: 'Jane Smith' },
      ]),
  })
);

describe('UserList', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('renders a list of users', async () => {
    const component = await UserList();
    const html = renderToString(component);

    expect(html).toContain('John Doe');
    expect(html).toContain('Jane Smith');
  });

  it('fetches data from the correct endpoint', async () => {
    await UserList();
    expect(fetch).toHaveBeenCalledWith(
      'https://jsonplaceholder.typicode.com/users'
    );
  });

  it('renders the correct number of items', async () => {
    const component = await UserList();
    const html = renderToString(component);
    const listItems = (html.match(/<li>/g) || []).length;
    expect(listItems).toBe(2);
  });
});

以 async 函数的方式调用组件可以在渲染之前获得其已解析的输出,这一点很重要,因为 RSC 返回的是一个解析为 JSX 的 Promise。使用 renderToString 渲染已解析的 JSX,能够模拟组件在生产环境中服务器端的处理方式。

使用 MSW 处理复杂的 API 场景

为了更真实的 API 模拟——多个端点、错误状态、网络延迟——MSW 比手动存根 fetch 更易维护:

// tests/mocks/handlers.js
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('https://jsonplaceholder.typicode.com/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'John Doe' },
      { id: 2, name: 'Jane Smith' },
    ]);
  }),
];
// tests/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
// jest.setup.js (updated)
import { server } from './tests/mocks/server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

有了这套配置,你的测试会与真实的模拟响应交互,并且可以在每个测试中覆盖处理函数,以模拟错误或边缘情况:

it('handles API errors gracefully', async () => {
  server.use(
    http.get('https://jsonplaceholder.typicode.com/users', () => {
      return new HttpResponse(null, { status: 500 });
    })
  );

  await expect(UserList()).rejects.toThrow();
});

测试使用 Next.js 服务器 API 的组件

当组件读取 cookies 或 headers 时,使用之前创建的 mock:

// app/components/AuthenticatedGreeting.js
import { cookies } from 'next/headers';

export default async function AuthenticatedGreeting() {
  const cookieStore = cookies();
  const session = cookieStore.get('session-token');

  if (!session) return <p>Please log in.</p>;
  return <p>Welcome back!</p>;
}
// __tests__/AuthenticatedGreeting.test.js
import { cookies } from 'next/headers';
import AuthenticatedGreeting from '@/components/AuthenticatedGreeting';
import { renderToString } from 'react-dom/server';

jest.mock('next/headers');

describe('AuthenticatedGreeting', () => {
  it('shows login prompt when no session exists', async () => {
    cookies.mockReturnValue({ get: jest.fn(() => null) });

    const component = await AuthenticatedGreeting();
    const html = renderToString(component);

    expect(html).toContain('Please log in.');
  });

  it('shows welcome message when session exists', async () => {
    cookies.mockReturnValue({
      get: jest.fn(() => ({ value: 'valid-token' })),
    });

    const component = await AuthenticatedGreeting();
    const html = renderToString(component);

    expect(html).toContain('Welcome back!');
  });
});

调试常见问题

问题解决方案
window is not defined将测试环境设置为 jsdom。将 jest.config.js 改为 testEnvironment: 'node'
Cannot read properties of undefined on Next.js imports__mocks__ 文件夹中或在测试文件顶部使用 jest.mock() 来模拟有问题的模块(next/headersnext/navigation 等)。
Async component not rendering correctly在传递给 renderToString 之前 await 组件:
const component = await MyComponent(props);
renderToString(component);
Fetch mock not being called确保在组件运行 之前global.fetch 赋值(例如,在 beforeEach 中或在 describe 块的顶部)。
Tests pass locally but fail in CI确认所有必需的环境变量在 CI 中可用,或在测试设置中显式模拟它们。

需要测试的内容(以及可以跳过的)

  • 值得测试的内容

    • 数据转换逻辑。
    • 基于获取的数据或服务器状态的条件渲染。
    • 错误和加载状态。
    • 正确的 API 调用。
  • 更适合通过集成或端到端测试处理

    • 完整的渲染管道。
    • 布局组合和路由行为。
    • 任何需要真实 Next.js 服务器的情况。

PlaywrightCypress 这样的工具更适合端到端场景。保持你的 Jest 单元测试专注于组件逻辑的孤立测试。

结论

测试 React Server Components 并没有比测试客户端组件困难得多——只需要不同的工具和稍微不同的思维模型。关键的转变有:

  1. 使用 node 测试环境,而不是 jsdom
  2. 直接调用异步组件 (await MyComponent()) 而不是使用 render()
  3. 明确地模拟 Next.js 服务器 API。

随着生态系统的成熟,预计这个过程会变得更顺畅。Next.js 团队和 React 核心团队正积极致力于为 RSC 提供更好的测试原语。目前,上述模式为你提供了可靠的基础,能够覆盖最常见的场景,而无需复杂的基础设施。

进一步阅读

0 浏览
Back to Blog

相关文章

阅读更多 »

‘skill-check’ JS 测验

问题 1:类型强制转换 以下代码在控制台会输出什么? javascript console.log0 == '0'; console.log0 === '0'; 答案:true,然后 false Ex...

与过去的最后之舞🕺

介绍 你好 dev.to 社区!一周前,我发布了我的第一篇文章,介绍了自己,并解释说我离开了网页开发,专注于 cryptography……