1. 設定 Jest 和 React 測試庫
npm install -D jest @jest/types ts-jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom
// jest.config.ts
import type { Config } from 'jest';
import nextJest from 'next/jest';
const createJestConfig = nextJest({ dir: './' });
const config: Config = {
setupFilesAfterSetup: ['<rootDir>/jest.setup.ts'],
testEnvironment: 'jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
export default createJestConfig(config);
// jest.setup.ts
import '@testing-library/jest-dom';
2. 單元測試組件
// __tests__/Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '@/components/Button';
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const user = userEvent.setup();
const onClick = jest.fn();
render(<Button onClick={onClick}>Click</Button>);
await user.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('is disabled when loading', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
});
3. 測試自訂鉤子
import { renderHook, act } from '@testing-library/react';
import { useCounter } from '@/hooks/useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
act(() => result.current.increment());
expect(result.current.count).toBe(2);
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(5));
act(() => result.current.increment());
act(() => result.current.reset());
expect(result.current.count).toBe(5);
});
});
4.MSW(模擬服務人員)
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/posts', () => {
return HttpResponse.json([
{ id: '1', title: 'Post 1' },
{ id: '2', title: 'Post 2' },
]);
}),
http.post('/api/posts', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: '3', ...body }, { status: 201 });
}),
];
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
5. 整合測試
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { server } from '@/mocks/server';
import { PostList } from '@/components/PostList';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('PostList', () => {
it('loads and displays posts', async () => {
render(<PostList />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Post 1')).toBeInTheDocument();
expect(screen.getByText('Post 2')).toBeInTheDocument();
});
});
});
6.劇作家端對端測試
// e2e/blog.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Blog', () => {
test('should display blog posts', async ({ page }) => {
await page.goto('/blog');
await expect(page.getByRole('heading', { name: 'Blog' })).toBeVisible();
await expect(page.locator('article')).toHaveCount(10);
});
test('should navigate to post detail', async ({ page }) => {
await page.goto('/blog');
await page.getByText('First Post').click();
await expect(page).toHaveURL(/\/blog\/.+/);
await expect(page.getByRole('article')).toBeVisible();
});
test('should search posts', async ({ page }) => {
await page.goto('/blog');
await page.getByPlaceholder('Tìm kiếm').fill('React');
await page.keyboard.press('Enter');
await expect(page.locator('article')).toHaveCount(3);
});
});
下一篇: 效能最佳化 — 核心 Web Vitals、延遲載入、捆綁分析。