「單元測試都過了,為什麼整合起來還是壞掉?」資深工程師搖搖頭,「因為你只測試了零件,沒測試組裝。」
今天,我們要把前 28 天學到的所有技巧組合起來,打造一個完整的 React 應用整合測試策略!
基礎測試 [✅] Kata 練習 [✅] 框架實戰 [✅] 整合部署 [🔄]
Day 1-10 Day 11-17 Day 18-27 Day 28-30
↑ 我們在這裡!
// 建立 tests/day29/integration-challenge.test.tsx
import { describe, it, expect } from 'vitest';
describe('Integration Challenges', () => {
it('shows why unit tests are not enough', () => {
// 單元測試 A: ✅ 通過
const formatDate = (date: Date) => date.toLocaleDateString();
// 單元測試 B: ✅ 通過
const calculateDays = (start: Date, end: Date) => {
return Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
};
// 整合時: ❌ 時區問題!
const startDate = new Date('2024-01-01T00:00:00Z');
const endDate = new Date('2024-01-02T23:59:59Z');
const days = calculateDays(startDate, endDate);
expect(days).toBe(1); // 可能失敗!
});
});
// 建立 tests/day29/todo-integration.test.tsx
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import TodoApp from '../../src/todo/TodoApp';
const server = setupServer(
http.get('/api/todos', () => {
return HttpResponse.json([
{ id: 1, title: '學習 TDD', completed: false },
{ id: 2, title: '寫測試', completed: true }
]);
}),
http.post('/api/todos', async ({ request }) => {
const body = await request.json() as { title: string };
return HttpResponse.json({
id: 3, title: body.title, completed: false
}, { status: 201 });
})
);
beforeEach(() => server.listen());
afterEach(() => server.resetHandlers());
afterEach(() => server.close());
describe('Todo App Integration', () => {
const renderTodoApp = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } }
});
return render(
<QueryClientProvider client={queryClient}>
<TodoApp />
</QueryClientProvider>
);
};
it('performs complete CRUD operations', async () => {
const user = userEvent.setup();
renderTodoApp();
// 載入初始資料
await waitFor(() => {
expect(screen.getByText('學習 TDD')).toBeInTheDocument();
});
// 新增待辦事項
const input = screen.getByPlaceholderText('新增待辦事項...');
await user.type(input, '整合測試');
await user.keyboard('{Enter}');
await waitFor(() => {
expect(screen.getByText('整合測試')).toBeInTheDocument();
});
// 標記完成
const todoItem = screen.getByText('學習 TDD').closest('li');
const checkbox = within(todoItem!).getByRole('checkbox');
await user.click(checkbox);
await waitFor(() => {
expect(checkbox).toBeChecked();
});
});
});
// 建立 tests/day29/cross-component.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TodoProvider } from '../../src/todo/TodoContext';
import TodoList from '../../src/todo/TodoList';
import TodoStats from '../../src/todo/TodoStats';
describe('Cross Component Communication', () => {
it('syncs statistics updates', async () => {
const user = userEvent.setup();
render(
<TodoProvider>
<TodoList />
<TodoStats />
</TodoProvider>
);
// 初始狀態
expect(screen.getByText(/總計: 0/i)).toBeInTheDocument();
// 新增待辦
const input = screen.getByPlaceholderText('新增待辦...');
await user.type(input, '測試項目');
await user.keyboard('{Enter}');
// 統計立即更新
await waitFor(() => {
expect(screen.getByText(/總計: 1/i)).toBeInTheDocument();
});
});
});
// 建立 tests/day29/routing-integration.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import App from '../../src/App';
import Settings from '../../src/Settings';
describe('Routing Integration', () => {
it('navigates through complete flow', async () => {
const user = userEvent.setup();
render(
<MemoryRouter initialEntries={['/']}>
<Routes>
<Route path="/" element={<App />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</MemoryRouter>
);
expect(screen.getByText(/待辦清單/i)).toBeInTheDocument();
const settingsLink = screen.getByRole('link', { name: /設定/i });
await user.click(settingsLink);
await waitFor(() => {
expect(screen.getByText(/應用程式設定/i)).toBeInTheDocument();
});
});
});
// 建立 tests/day29/state-integration.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from '../../src/store/todoSlice';
import userReducer from '../../src/store/userSlice';
import Dashboard from '../../src/Dashboard';
describe('State Management Integration', () => {
it('syncs cross slice state', async () => {
const store = configureStore({
reducer: { todos: todoReducer, user: userReducer }
});
const user = userEvent.setup();
render(<Provider store={store}><Dashboard /></Provider>);
const loginButton = screen.getByRole('button', { name: /登入/i });
await user.click(loginButton);
await waitFor(() => {
expect(screen.getByText(/歡迎, TestUser/i)).toBeInTheDocument();
expect(screen.getByText(/個人待辦事項/i)).toBeInTheDocument();
});
});
});
// 完整實作 tests/day29/complete-integration-suite.test.tsx
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from '../../src/store';
import ErrorBoundary from '../../src/ErrorBoundary';
import App from '../../src/App';
const createTestWrapper = () => {
const store = configureStore({ reducer: rootReducer });
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } }
});
return ({ children }: { children: React.ReactNode }) => (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<ErrorBoundary>{children}</ErrorBoundary>
</MemoryRouter>
</QueryClientProvider>
</Provider>
);
};
const server = setupServer();
beforeEach(() => server.listen());
afterEach(() => server.resetHandlers());
afterEach(() => server.close());
describe('Complete Integration Suite', () => {
it('completes end to end user flow', async () => {
const user = userEvent.setup();
// 設定 API 模擬
server.use(
http.post('/api/login', async ({ request }) => {
const body = await request.json() as { username: string };
return HttpResponse.json({
token: 'test-token',
user: { id: 1, name: 'Test User' }
});
}),
http.get('/api/todos', () => {
return HttpResponse.json([
{ id: 1, title: '整合測試', completed: false }
]);
})
);
const TestWrapper = createTestWrapper();
render(<App />, { wrapper: TestWrapper });
// 登入流程
const usernameInput = screen.getByLabelText(/使用者名稱/i);
const loginButton = screen.getByRole('button', { name: /登入/i });
await user.type(usernameInput, 'test');
await user.click(loginButton);
// 驗證登入成功
await waitFor(() => {
expect(screen.getByText(/歡迎, Test User/i)).toBeInTheDocument();
});
// 載入待辦事項
await waitFor(() => {
expect(screen.getByText('整合測試')).toBeInTheDocument();
});
// 互動測試
const todoCheckbox = screen.getByRole('checkbox');
await user.click(todoCheckbox);
await waitFor(() => {
expect(todoCheckbox).toBeChecked();
});
});
});
今天我們完成了完整的整合測試實戰:
明天是我們旅程的最後一天,我們將部署整個應用並總結這 30 天的學習!