在 Next.js 中测试 React Server Components
Source: Dev.to
(请提供您希望翻译的正文内容,我将为您翻译成简体中文,并保留原始的格式、代码块和链接。)
理解 React Server Components
在编写测试之前,先了解一下 RSC 与客户端组件的区别会很有帮助:
- 没有客户端状态或生命周期方法。 RSC 不能使用
useState、useEffect或任何依赖浏览器环境的 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/headers、next/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 服务器的情况。
像 Playwright 或 Cypress 这样的工具更适合端到端场景。保持你的 Jest 单元测试专注于组件逻辑的孤立测试。
结论
测试 React Server Components 并没有比测试客户端组件困难得多——只需要不同的工具和稍微不同的思维模型。关键的转变有:
- 使用
node测试环境,而不是jsdom。 - 直接调用异步组件 (
await MyComponent()) 而不是使用render()。 - 明确地模拟 Next.js 服务器 API。
随着生态系统的成熟,预计这个过程会变得更顺畅。Next.js 团队和 React 核心团队正积极致力于为 RSC 提供更好的测试原语。目前,上述模式为你提供了可靠的基础,能够覆盖最常见的场景,而无需复杂的基础设施。