技術 6 分鐘閱讀
React 測試入門:用測試保護你的程式碼
前言
老實說,接案工程師寫測試的比例並不高。時間壓力大、覺得手動測就好——我以前也是這樣想。直到有一次在線上環境改了一個小功能,結果意外破壞了另一個功能,才深刻體會到測試的重要性。
為什麼要寫測試?
- 防止回歸錯誤:修改程式碼時不怕弄壞其他功能
- 文件化:測試本身就是最好的使用範例
- 重構信心:有測試保護,重構才敢放手做
- 提升程式碼品質:可測試的程式碼通常設計更好
工具選擇
推薦組合
- Vitest:取代 Jest,與 Vite 完美整合,速度更快
- React Testing Library:以使用者角度測試元件
- MSW(Mock Service Worker):模擬 API 請求
安裝
npm install -D vitest @testing-library/react @testing-library/jest-dom
npm install -D @testing-library/user-event jsdom
設定
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.ts',
},
});
// src/test/setup.ts
import '@testing-library/jest-dom';
第一個測試
簡單的元件測試
// components/Greeting.tsx
interface GreetingProps {
name: string;
}
export function Greeting({ name }: GreetingProps) {
return <h1>你好,{name}!</h1>;
}
// components/Greeting.test.tsx
import { render, screen } from '@testing-library/react';
import { Greeting } from './Greeting';
describe('Greeting', () => {
it('顯示使用者名稱', () => {
render(<Greeting name="小明" />);
expect(screen.getByText('你好,小明!')).toBeInTheDocument();
});
});
執行測試:
npx vitest
測試使用者互動
// components/Counter.tsx
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>目前計數:{count}</p>
<button onClick={() => setCount(c => c + 1)}>增加</button>
<button onClick={() => setCount(c => c - 1)}>減少</button>
<button onClick={() => setCount(0)}>重設</button>
</div>
);
}
// components/Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter', () => {
it('初始計數為 0', () => {
render(<Counter />);
expect(screen.getByText('目前計數:0')).toBeInTheDocument();
});
it('點擊增加按鈕後計數加 1', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByText('增加'));
expect(screen.getByText('目前計數:1')).toBeInTheDocument();
});
it('點擊減少按鈕後計數減 1', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByText('減少'));
expect(screen.getByText('目前計數:-1')).toBeInTheDocument();
});
it('重設按鈕將計數歸零', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByText('增加'));
await user.click(screen.getByText('增加'));
await user.click(screen.getByText('重設'));
expect(screen.getByText('目前計數:0')).toBeInTheDocument();
});
});
測試表單
// components/LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
const mockOnSubmit = vi.fn();
beforeEach(() => {
mockOnSubmit.mockClear();
});
it('送出表單時呼叫 onSubmit', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('密碼'), 'password123');
await user.click(screen.getByRole('button', { name: '登入' }));
expect(mockOnSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
it('Email 為空時顯示錯誤訊息', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.click(screen.getByRole('button', { name: '登入' }));
expect(screen.getByText('Email 為必填')).toBeInTheDocument();
expect(mockOnSubmit).not.toHaveBeenCalled();
});
});
測試 API 呼叫
使用 MSW 來模擬 API:
npm install -D msw
// test/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/products', () => {
return HttpResponse.json([
{ id: 1, name: '商品 A', price: 100 },
{ id: 2, name: '商品 B', price: 200 },
]);
}),
];
// test/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// src/test/setup.ts
import '@testing-library/jest-dom';
import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// components/ProductList.test.tsx
import { render, screen } from '@testing-library/react';
import { ProductList } from './ProductList';
describe('ProductList', () => {
it('顯示商品列表', async () => {
render(<ProductList />);
expect(await screen.findByText('商品 A')).toBeInTheDocument();
expect(screen.getByText('商品 B')).toBeInTheDocument();
});
});
測試策略
該測什麼?
- 使用者行為:點擊按鈕、填寫表單、看到結果
- 條件渲染:不同狀態顯示不同內容
- 錯誤處理:API 失敗時的顯示
不該測什麼?
- 實作細節:state 的值、元件內部的函式
- 第三方套件:相信它們有自己的測試
- 純 CSS 樣式:除非影響功能
測試金字塔
- 單元測試(多):測試個別元件和函式
- 整合測試(適中):測試元件之間的互動
- E2E 測試(少):測試完整的使用者流程
結語
寫測試一開始會覺得花時間,但長期來看絕對值得。特別是接案專案需要長期維護時,測試能讓你安心地修改和更新程式碼。從最重要的功能開始寫測試,慢慢建立起測試的習慣和信心。
#React #測試 #Vitest #Testing Library