测试你的 Next.-js 应用:Jest 和 React Testing Library
测试你的 Next.-js 应用:Jest 和 React Testing Library
作者:码力无边
到目前为止,我们已经构建了功能丰富的页面、API 和组件。我们的应用在本地开发环境中运行良好,但我们如何确保在未来的代码修改、重构或添加新功能时,不会无意中破坏现有的功能呢?答案就是——自动化测试。
编写测试是专业软件开发的基石。它能:
- 保证代码质量:在代码合并和部署前捕获 bug。
- 提供安全网:让你能够自信地进行重构,因为测试会告诉你是否破坏了某些东西。
- 充当文档:好的测试本身就是对组件行为的最佳描述。
- 促进更好的设计:为了让代码可测试,你通常需要编写更模块化、更解耦的代码。
在 React 和 Next.js 的生态中,最流行和推荐的测试组合是 Jest 和 React Testing Library (RTL)。
- Jest:一个功能全面的 JavaScript 测试运行器 (Test Runner)。它提供了测试结构 (
describe
,it
,test
)、断言库 (expect
) 和 mock (模拟) 功能。 - React Testing Library (RTL):一个专注于测试 React 组件的库。它的核心哲学是:“测试你的软件,就像用户使用它一样”。它鼓励你通过查询和交互的方式来测试组件,而不是深入其内部实现细节。
本文将指导你在 Next.js App Router 项目中配置 Jest 和 RTL,并编写几种常见的测试类型,包括对工具函数、React 组件和 API 路由的测试。
步骤一:环境配置
Next.js 官方提供了一个与 Jest 集成的示例,我们可以参考它来快速配置。
-
安装开发依赖
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @types/jest
jest
,@types/jest
:Jest 核心。jest-environment-jsdom
:让 Jest 可以在一个模拟的浏览器环境 (JSDOM) 中运行,这对于测试 DOM 操作至关重要。@testing-library/react
:RTL 的核心,用于渲染组件和交互。@testing-library/jest-dom
:为 Jest 的expect
提供了许多方便的、用于断言 DOM 状态的匹配器(如toBeInTheDocument()
)。
-
配置 Jest (
jest.config.js
)在项目根目录创建
jest.config.js
文件。Next.js 官方提供了一个next/jest
辅助函数,可以帮助我们处理 Babel、TypeScript 等复杂配置。// jest.config.js const nextJest = require('next/jest');const createJestConfig = nextJest({// 提供 next.config.js 和 .env 文件相对于此目录的路径dir: './', });// 添加任何你想自定义的 Jest 配置 const customJestConfig = {setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],testEnvironment: 'jest-environment-jsdom', };// createJestConfig 被包裹以确保 next/jest 可以加载 Next.js 的配置 module.exports = createJestConfig(customJestConfig);
-
创建 Jest 设置文件 (
jest.setup.js
)这个文件会在每个测试文件运行前执行,是放置全局设置的理想场所。我们在这里导入
@testing-library/jest-dom
来扩展expect
。// jest.setup.js import '@testing-library/jest-dom';
-
配置 TypeScript (
tsconfig.json
)
为了避免类型冲突,建议在tsconfig.json
的include
数组中加入 Jest 配置文件。{"compilerOptions": { ... },"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "jest.config.js", "jest.setup.js"],"exclude": ["node_modules"] }
-
在
package.json
中添加测试脚本"scripts": {"dev": "next dev","build": "next build","start": "next start","lint": "next lint","test": "jest","test:watch": "jest --watch" }
配置完成!现在我们可以开始编写测试了。
实战一:测试简单的工具函数 (单元测试)
这是最简单的测试类型。假设我们有一个计算总价的工具函数。
lib/utils.ts
export function calculateTotalPrice(price: number, quantity: number): number {if (price < 0 || quantity < 0) {throw new Error('价格和数量不能为负数');}return price * quantity;
}
__tests__/lib/utils.test.ts
(通常将测试文件放在 __tests__
目录下,或与源文件同级的 .test.ts
文件)
import { calculateTotalPrice } from '../../lib/utils';// `describe` 用于将相关的测试分组
describe('calculateTotalPrice', () => {// `it` 或 `test` 用于定义一个测试用例it('应该正确计算总价', () => {// 断言:期望 10 * 2 的结果是 20expect(calculateTotalPrice(10, 2)).toBe(20);});it('应该能处理数量为0的情况', () => {expect(calculateTotalPrice(10, 0)).toBe(0);});it('当输入为负数时应该抛出错误', () => {// 测试函数是否按预期抛出错误expect(() => calculateTotalPrice(-10, 2)).toThrow('价格和数量不能为负数');});
});
运行 npm test
,你应该能看到测试通过的绿色提示。
实战二:测试 React 组件 (组件测试)
这是 RTL 的核心舞台。我们将测试一个简单的计数器组件。
components/Counter.tsx
"use client";
import { useState } from 'react';export default function Counter() {const [count, setCount] = useState(0);return (<div><p>当前计数: {count}</p><button onClick={() => setCount(count + 1)}>增加</button></div>);
}
__tests__/components/Counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from '../../components/Counter';describe('Counter component', () => {it('应该正确渲染初始状态', () => {// 1. 渲染组件render(<Counter />);// 2. 查询元素// screen 对象提供了多种查询方法,getByText 是其中之一const countElement = screen.getByText(/当前计数: 0/i);const buttonElement = screen.getByRole('button', { name: /增加/i });// 3. 断言expect(countElement).toBeInTheDocument();expect(buttonElement).toBeInTheDocument();});it('点击按钮后计数应该增加', () => {render(<Counter />);const buttonElement = screen.getByRole('button', { name: /增加/i });// 模拟用户行为:点击按钮fireEvent.click(buttonElement);// 查询更新后的元素const countElement = screen.getByText(/当前计数: 1/i);// 断言状态已更新expect(countElement).toBeInTheDocument();});
});
RTL 核心思想:
render
: 将你的组件渲染到 JSDOM 中。screen
: 提供查询 DOM 的方法,鼓励使用用户可见的方式查询(如getByRole
,getByText
,getByLabelText
),而不是getByTestId
或 class/id 选择器。fireEvent
/userEvent
: 模拟用户交互,如点击、输入等 (user-event
是一个更接近真实用户行为的库,推荐在复杂交互中使用)。
实战三:测试 Server Components (及数据获取)
测试 Server Components 稍微复杂一些,因为它们是 async
的,并且可能依赖外部数据。我们需要使用 mock
来模拟 fetch
或其他数据源。
app/posts/[id]/page.tsx
(一个简化的文章详情页)
async function getPost(id: string) {const res = await fetch(`https://api.example.com/posts/${id}`);if (!res.ok) throw new Error('Failed to fetch');return res.json();
}export default async function PostPage({ params }: { params: { id: string } }) {const post = await getPost(params.id);return (<article><h1>{post.title}</h1><p>{post.body}</p></article>);
}
__tests__/app/posts/[id]/page.test.tsx
import { render, screen } from '@testing-library/react';
import PostPage from '../../../../app/posts/[id]/page';// Mock 全局的 fetch 函数
global.fetch = jest.fn();describe('PostPage', () => {it('应该能正确获取并渲染文章数据', async () => {const mockPost = {id: 1,title: '我的第一篇测试文章',body: '这是文章的内容。',};// 配置 fetch mock 的返回值(global.fetch as jest.Mock).mockResolvedValueOnce({ok: true,json: async () => mockPost,});// Server Components 返回 Promise,所以我们需要 await render 的结果const PageComponent = await PostPage({ params: { id: '1' } });render(PageComponent);// 使用 findBy* 方法,它会等待异步操作完成const titleElement = await screen.findByRole('heading', { name: mockPost.title });const bodyElement = screen.getByText(mockPost.body);expect(titleElement).toBeInTheDocument();expect(bodyElement).toBeInTheDocument();});
});
总结
测试是构建高质量、可维护的 Next.js 应用不可或缺的一环。虽然初期的配置和学习需要投入一些时间,但它带来的长期收益是巨大的。
核心测试策略回顾:
- 单元测试:使用 Jest 测试独立的、纯粹的逻辑(如工具函数),确保其输入和输出符合预期。
- 组件测试:使用 React Testing Library 测试组件。遵循“像用户一样测试”的原则,关注组件的渲染输出和交互行为,而非其内部实现。
- Mocking:当测试依赖于外部系统(如 API, 数据库)时,使用 Jest 的 mock 功能来模拟这些依赖,使测试变得确定和快速。
将自动化测试集成到你的开发流程中,你将能够更自信地发布新版本,更快地迭代产品,并构建出更健壮、更可靠的应用程序。
在下一篇文章中,我们将探讨国际化 (i18n),学习如何构建一个支持多种语言的 Next.js 网站,以触达更广泛的全球用户。敬请期待!