剛開始寫前端測試時,總覺得是多此一舉——代碼能跑起來不就行了?直到一次上線後,一個簡單的表單驗證邏輯出錯,導致用户無法提交數據,才意識到測試的重要性。前端測試不僅能幫我們提前發現問題,還能讓代碼結構更清晰,重構時更有底氣。Jest 配合 React Testing Library 是目前 React 項目最流行的測試組合,前者負責測試運行和斷言,後者專注於模擬用户行為,兩者結合能寫出既可靠又貼近實際使用場景的測試。
一、環境準備:搭建測試框架
首先在 React 項目中安裝必要的依賴:
# 創建 React 項目(如果還沒有)
npx create-react-app my-app
cd my-app
# 安裝測試相關依賴(create-react-app 已內置 Jest)
# React Testing Library 相關包
npm install @testing-library/react @testing-library/jest-dom @testing-library/user-event --save-dev
安裝完成後,package.json 中會自動生成測試腳本,運行 npm test 即可啓動測試。
二、測試基礎:從一個按鈕組件開始
先從簡單的組件入手,比如一個點擊後會改變文本的按鈕組件:
// Button.js
import { useState } from 'react';
function Button() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
點擊了 {count} 次
</button>
);
}
export default Button;
對應的測試文件 Button.test.js 放在同一目錄下,測試內容包括:組件是否渲染、點擊後文本是否變化。
// Button.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';
// 測試套件
describe('Button 組件', () => {
// 測試用例 1:組件能正常渲染
test('渲染初始文本', () => {
// 渲染組件
render(<Button />);
// 查找文本內容為“點擊了 0 次”的元素
const buttonElement = screen.getByText(/點擊了 0 次/i);
// 斷言元素存在
expect(buttonElement).toBeInTheDocument();
});
// 測試用例 2:點擊後計數增加
test('點擊後計數加 1', async () => {
// 初始化用户事件模擬
const user = userEvent.setup();
render(<Button />);
const buttonElement = screen.getByText(/點擊了 0 次/i);
// 模擬用户點擊
await user.click(buttonElement);
// 斷言文本變為“點擊了 1 次”
expect(screen.getByText(/點擊了 1 次/i)).toBeInTheDocument();
});
});
運行 npm test 後,測試會通過,顯示兩個測試用例都成功。這裏的關鍵點是:
- 用
render渲染組件到測試環境的 DOM 中 - 用
screen.getByText查找元素(類似用户通過文本識別元素) - 用
userEvent.click模擬真實用户點擊(比fireEvent更貼近實際行為) - 用
expect斷言結果是否符合預期
三、表單測試:驗證用户輸入
表單是前端最常見的組件,測試重點包括輸入驗證、提交邏輯等。以一個簡單的登錄表單為例:
// LoginForm.js
import { useState } from 'react';
function LoginForm({ onSubmit }) {
const [formData, setFormData] = useState({
username: '',
password: ''
});
const [error, setError] = useState('');
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
if (!formData.username || !formData.password) {
setError('用户名和密碼不能為空');
return;
}
setError('');
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
{error && <div className="error" role="alert">{error}</div>}
<div>
<label htmlFor="username">用户名</label>
<input
id="username"
name="username"
value={formData.username}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="password">密碼</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
</div>
<button type="submit">登錄</button>
</form>
);
}
export default LoginForm;
測試這個組件需要覆蓋:空值提交顯示錯誤、輸入內容後能正常提交。
// LoginForm.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
describe('LoginForm 組件', () => {
test('提交空表單顯示錯誤', async () => {
const user = userEvent.setup();
// 模擬提交回調
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
// 點擊提交按鈕
await user.click(screen.getByRole('button', { name: /登錄/i }));
// 斷言錯誤信息顯示
expect(screen.getByRole('alert')).toHaveTextContent(/用户名和密碼不能為空/i);
// 斷言回調沒被調用
expect(mockSubmit).not.toHaveBeenCalled();
});
test('輸入內容後能正常提交', async () => {
const user = userEvent.setup();
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
// 輸入用户名
await user.type(screen.getByLabelText(/用户名/i), 'testuser');
// 輸入密碼
await user.type(screen.getByLabelText(/密碼/i), 'testpass');
// 提交表單
await user.click(screen.getByRole('button', { name: /登錄/i }));
// 斷言錯誤信息消失
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
// 斷言回調被正確調用
expect(mockSubmit).toHaveBeenCalledWith({
username: 'testuser',
password: 'testpass'
});
});
});
這裏用到了幾個實用技巧:
- 用
getByLabelText通過標籤查找輸入框(符合用户通過標籤識別輸入框的習慣) - 用
user.type模擬用户輸入文本 - 用
jest.fn()創建模擬函數,通過toHaveBeenCalledWith驗證調用參數 - 用
queryByRole代替getByRole檢查元素是否不存在(getBy找不到會報錯)
四、異步測試:處理 API 請求
實際項目中很多組件會調用 API,測試時需要模擬異步請求。以一個加載用户列表的組件為例:
// UserList.js
import { useState, useEffect } from 'react';
import axios from 'axios';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
axios.get('/api/users')
.then(res => {
setUsers(res.data);
setLoading(false);
})
.catch(err => {
setError('加載失敗');
setLoading(false);
});
}, []);
if (loading) return <div>加載中...</div>;
if (error) return <div role="alert">{error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default UserList;
測試時需要模擬 axios.get 方法,分別測試加載中、加載成功、加載失敗三種狀態:
// UserList.test.js
import { render, screen, waitFor } from '@testing-library/react';
import axios from 'axios';
import UserList from './UserList';
// 模擬 axios 模塊
jest.mock('axios');
describe('UserList 組件', () => {
test('顯示加載狀態', () => {
render(<UserList />);
expect(screen.getByText(/加載中.../i)).toBeInTheDocument();
});
test('加載成功顯示用户列表', async () => {
// 模擬 axios 返回成功數據
axios.get.mockResolvedValue({
data: [
{ id: 1, name: '張三' },
{ id: 2, name: '李四' }
]
});
render(<UserList />);
// 等待加載完成,用户列表出現
await waitFor(() => {
expect(screen.getByText('張三')).toBeInTheDocument();
});
expect(screen.getByText('李四')).toBeInTheDocument();
});
test('加載失敗顯示錯誤信息', async () => {
// 模擬 axios 返回失敗
axios.get.mockRejectedValue(new Error('網絡錯誤'));
render(<UserList />);
// 等待錯誤信息出現
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/加載失敗/i);
});
});
});
這裏的關鍵是用 jest.mock('axios') 替換真實的 axios 模塊,再通過 mockResolvedValue 或 mockRejectedValue 模擬不同的返回結果。waitFor 用於等待異步操作完成,確保斷言在正確的時機執行。
五、測試原則與最佳實踐
- 測試行為而非實現:關注組件“做什麼”而不是“怎麼做”。比如測試按鈕點擊後文本變化,而不是測試
count狀態是否加 1——即使內部實現從useState改成useReducer,測試依然有效。 - 模擬用户真實行為:用
userEvent而不是fireEvent,前者更貼近用户實際操作(比如輸入文本會觸發change事件和光標移動)。 - 保持測試獨立:每個測試用例應該互不影響,避免一個測試的結果影響另一個。可以用
beforeEach重置狀態。 - 只測必要的內容:不必追求 100% 覆蓋率,重點測試核心功能(如支付流程、表單提交),簡單的展示組件可以少測。
- 使用語義化查詢:優先用
getByRole(按角色,如按鈕、輸入框)、getByLabelText(按標籤)查找元素,而不是getByClassName或getByTestId——前者更符合用户體驗,也更能適應代碼重構。
總結
Jest + React Testing Library 讓前端測試變得簡單直觀,核心不是記住多少 API,而是理解“測試用户行為”這個理念。從簡單的組件開始,逐步覆蓋表單、異步請求等場景,你會發現測試其實是開發過程的一部分,能幫你更早發現問題,讓代碼更健壯。
剛開始寫測試可能覺得麻煩,但隨着項目變大,測試帶來的收益會越來越明顯——重構時不用手動點點點驗證功能,改完代碼跑一遍測試就知道有沒有問題。這大概就是為什麼成熟的前端團隊都離不開測試吧。