تستنویسی در javascript/typescript — بخش ۷: MSW و پروژه Todo List
توی بخش ۶ یاد گرفتیم با vi.fn() و global.fetch =... شبکه رو mock کنیم. کار میکنه، ولی وقتی API زیاد بشه هر تست پر میشه از کد تکراری mock.
به جاش میرم سراغ Mock Service Worker یا همون MSW.
MSW درخواستهای شبکه رو قبل از رسیدن به اینترنت میگیره و جواب fake برمیگردونه. کد شما همون fetch معمولی رو میزنه؛ فقط جواب از mock میآد.
توی این مقاله اول MSW رو میبینیم، بعد با هم یه Todo List با React مینویسیم.
MSW چیه و چرا بهتر از mock دستی fetch؟
مشکل mock دستی (بخش ۶)
// هر فایل تست دوباره این رو مینویسه...
global.fetch = vi.fn(() => Promise.resolve({ json: () => ... }));
- تکرار زیاد
- فراموش کردن restore
- سخت شدن نگهداری وقتی endpoint زیاد میشه
راهحل MSW
- یک جا handlerها رو تعریف میکنی (
/api/todos،/api/login،...) - کد production عوض نمیشه؛ همون
fetch('/api/todos')میمونه - نزدیکتر به دنیای واقعی، چون فقط لایه شبکه intercept میشه
| mock دستی fetch | MSW | |
|---|---|---|
| محل تعریف | هر تست | فایل handlers مرکزی |
| کد app | بدون تغییر | بدون تغییر |
| چند endpoint | شلوغ | منظم |
| توصیه | تست کوچک | پروژه واقعی با API |
نصب و راهاندازی MSW
npm install --save-dev msw
ساختار پوشه
src/
├── mocks/
│ ├── handlers.js # تعریف endpointها
│ └── server.js # سرور تست
├── components/
│ └── ToDoList.jsx
└── tests/
└── setupTests.js
handlers: src/mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ id: '123', name: 'John Doe' }));
}),
rest.post('/api/login', (req, res, ctx) => {
const { username } = req.body;
return res(ctx.status(200), ctx.json({ message: `Welcome, ${username}!` }));
}),
];
server: src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
setupTests: tests/setupTests.js
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from '../src/mocks/server';
import '@testing-library/jest-dom/vitest';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
onUnhandledRequest: 'error' یعنی اگر تستی به APIای زد که handler نداریم، fail بشه. برای پیدا کردن باگ خیلی بهدردبخوره.
vitest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setupTests.js',
},
});
اولین تست با MSW: UserProfile
یه کامپوننت ساده نشون میده:
// UserProfile.jsx
import { useEffect, useState } from 'react';
export function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then((res) => res.json())
.then(setUser);
}, []);
if (!user) return <div>Loading…</div>;
return (
<div>
<h1>{user.name}</h1>
</div>
);
}
// UserProfile.test.jsx
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import { UserProfile } from './UserProfile';
test('renders user profile after fetching data', async () => {
render(<UserProfile />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const userName = await screen.findByText('John Doe');
expect(userName).toBeInTheDocument();
});
بدون هیچ vi.fn روی fetch. handler در handlers.js جواب { name: 'John Doe' } رو میده.
پروژه Todo List: شروع
حالا مثال اصلی: Todo List با React، TDD و MSW.
نصب
npm create vite@latest todo-app-tdd -- --template react
cd todo-app-tdd
npm install
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom msw
API که شبیهسازی میکنیم
| Method | Endpoint | کار |
|---|---|---|
| GET | /api/todos |
لیست todoها |
| POST | /api/todos |
اضافه کردن |
| PUT | /api/todos/:id |
آپدیت (مثلاً completed) |
| DELETE | /api/todos/:id |
حذف |
داده mock
const mockTodos = [
{ id: 1, title: 'Buy groceries', completed: false },
{ id: 2, title: 'Walk the dog', completed: true },
];
مرحله ۱: نمایش لیست Todo (TDD)
تست (قرمز)
// src/tests/ToDoList.test.jsx
import { render, screen } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ToDoList } from '../components/ToDoList';
const mockTodos = [
{ id: 1, title: 'Buy groceries', completed: false },
{ id: 2, title: 'Walk the dog', completed: true },
];
const server = setupServer(
rest.get('/api/todos', (req, res, ctx) => {
return res(ctx.json(mockTodos));
}),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('renders to-do items fetched from API', async () => {
render(<ToDoList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const items = await screen.findAllByRole('listitem');
expect(items).toHaveLength(2);
expect(screen.getByText('Buy groceries')).toBeInTheDocument();
expect(screen.getByText('Walk the dog')).toBeInTheDocument();
});
کد (سبز): ToDoList.jsx
import { useEffect, useState } from 'react';
export function ToDoList() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/todos')
.then((res) => res.json())
.then((data) => {
setTodos(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading…</div>;
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.title} {todo.completed ? '(Completed)' : ''}
</li>
))}
</ul>
);
}
تست pass میشه. ساده بود!
مرحله ۲: اضافه کردن Todo جدید
تست
import userEvent from '@testing-library/user-event';
test('adds a new to-do item', async () => {
const user = userEvent.setup();
render(<ToDoList />);
await screen.findByText('Buy groceries');
const input = screen.getByPlaceholderText('Add new to-do');
const addButton = screen.getByRole('button', { name: /add/i });
server.use(
rest.post('/api/todos', (req, res, ctx) => {
return res(ctx.status(201), ctx.json({ id: 3, title: 'Learn TDD', completed: false }));
}),
);
server.use(
rest.get('/api/todos', (req, res, ctx) => {
return res(ctx.json([...mockTodos, { id: 3, title: 'Learn TDD', completed: false }]));
}),
);
await user.type(input, 'Learn TDD');
await user.click(addButton);
expect(await screen.findByText('Learn TDD')).toBeInTheDocument();
});
نکته: server.use برای یک تست
server.use handler رو موقتاً عوض میکنه. بعد resetHandlers در afterEach برمیگرده به حالت قبل.
کد بهروز شده
export function ToDoList() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
const [newTodo, setNewTodo] = useState('');
useEffect(() => {
fetchTodos();
}, []);
function fetchTodos() {
fetch('/api/todos')
.then((res) => res.json())
.then((data) => {
setTodos(data);
setLoading(false);
});
}
function addTodo() {
fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTodo }),
}).then(() => {
setNewTodo('');
fetchTodos();
});
}
if (loading) return <div>Loading…</div>;
return (
<div>
<input
placeholder="Add new to-do"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
/>
<button type="button" onClick={addTodo}>Add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.title} {todo.completed ? '(Completed)' : ''}
</li>
))}
</ul>
</div>
);
}
مرحله ۳: علامتزدن بهعنوان انجامشده
تست (قرمز)
test('marks a to-do item as completed', async () => {
const user = userEvent.setup();
render(<ToDoList />);
const itemCheckbox = await screen.findByRole('checkbox', { name: 'Buy groceries' });
expect(itemCheckbox).not.toBeChecked();
server.use(
rest.put('/api/todos/1', (req, res, ctx) => {
return res(ctx.json({ id: 1, title: 'Buy groceries', completed: true }));
}),
);
await user.click(itemCheckbox);
expect(itemCheckbox).toBeChecked();
});
اول fail میشه، چون checkbox هنوز نیست.
کد (سبز): toggleComplete
function toggleComplete(todo) {
fetch(`/api/todos/${todo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...todo, completed: !todo.completed }),
}).then(() => {
fetchTodos();
});
}
// در JSX:
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleComplete(todo)}
aria-label={todo.title}
/>
{todo.title}
</label>
</li>
))}
</ul>
تست pass میشه. نکته: aria-label={todo.title} مهمه، چون Testing Library با اسم checkbox پیداش میکنه.
مرحله ۴: حذف Todo
تست (قرمز)
test('deletes a to-do item', async () => {
const user = userEvent.setup();
render(<ToDoList />);
const deleteButton = await screen.findByRole('button', { name: 'Delete Buy groceries' });
server.use(
rest.delete('/api/todos/1', (req, res, ctx) => {
return res(ctx.status(200));
}),
);
server.use(
rest.get('/api/todos', (req, res, ctx) => {
return res(ctx.json(mockTodos.filter((todo) => todo.id !== 1)));
}),
);
await user.click(deleteButton);
expect(screen.queryByText('Buy groceries')).not.toBeInTheDocument();
});
کد (سبز): deleteTodo
function deleteTodo(id) {
fetch(`/api/todos/${id}`, {
method: 'DELETE',
}).then(() => {
fetchTodos();
});
}
// در JSX:
<button
type="button"
onClick={() => deleteTodo(todo.id)}
aria-label={`Delete ${todo.title}`}
>
Delete
</button>
اگر کامپوننت شلوغ شد، پیشنهاد میکنم API callها رو ببری توی یه فایل todoService.js. ولی اول TDD رو تموم کن، بعد refactor.
کامپوننت نهایی ToDoList: یکجا
import { useEffect, useState } from 'react';
export function ToDoList() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
const [newTodo, setNewTodo] = useState('');
useEffect(() => {
fetchTodos();
}, []);
function fetchTodos() {
fetch('/api/todos')
.then((res) => res.json())
.then((data) => {
setTodos(data);
setLoading(false);
});
}
function addTodo() {
fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTodo }),
}).then(() => {
setNewTodo('');
fetchTodos();
});
}
function toggleComplete(todo) {
fetch(`/api/todos/${todo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...todo, completed: !todo.completed }),
}).then(() => fetchTodos());
}
function deleteTodo(id) {
fetch(`/api/todos/${id}`, { method: 'DELETE' }).then(() => fetchTodos());
}
if (loading) return <div>Loading…</div>;
return (
<div>
<input
placeholder="Add new to-do"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
/>
<button type="button" onClick={addTodo}>Add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleComplete(todo)}
aria-label={todo.title}
/>
{todo.title}
</label>
<button
type="button"
onClick={() => deleteTodo(todo.id)}
aria-label={`Delete ${todo.title}`}
>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
چهار feature داریم: خواندن، اضافه، complete، حذف. همه با TDD.
handlers مرکزی برای Todo
به جای تعریف server در هر فایل تست، میتونی همه رو بذاری توی handlers.js:
// src/mocks/handlers.js
import { rest } from 'msw';
let todos = [
{ id: 1, title: 'Buy groceries', completed: false },
{ id: 2, title: 'Walk the dog', completed: true },
];
export const handlers = [
rest.get('/api/todos', (req, res, ctx) => {
return res(ctx.json(todos));
}),
rest.post('/api/todos', async (req, res, ctx) => {
const { title } = await req.json();
const newTodo = { id: Date.now(), title, completed: false };
todos.push(newTodo);
return res(ctx.status(201), ctx.json(newTodo));
}),
rest.put('/api/todos/:id', async (req, res, ctx) => {
const { id } = req.params;
const body = await req.json();
todos = todos.map((t) => (t.id === Number(id) ? { ...t, ...body } : t));
const updated = todos.find((t) => t.id === Number(id));
return res(ctx.json(updated));
}),
rest.delete('/api/todos/:id', (req, res, ctx) => {
const { id } = req.params;
todos = todos.filter((t) => t.id !== Number(id));
return res(ctx.status(204));
}),
];
توی beforeEach میتونی todos رو reset کنی تا تستها مستقل بمونن.
تست خطای API با MSW
test('shows error when API fails', async () => {
server.use(
rest.get('/api/todos', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Server error' }));
}),
);
render(<ToDoList />);
expect(await screen.findByText(/error/i)).toBeInTheDocument();
});
فرض اینه که کامپوننت پیام خطا نشون میده. اگر نه، اول اون رو اضافه کن (پایینتر پیادهسازیش هست).
MSW در برابر mock fetch: کی کدوم؟
| موقعیت | پیشنهاد |
|---|---|
| یک تابع کوچک با یک fetch | vi.fn کافیه (بخش ۶) |
| کامپوننت React + چند endpoint | MSW |
| میخوای هم dev هم test یک mock داشته باشی | MSW (حتی در browser هم کار میکنه) |
خطاهای رایج
۱. فراموش کردن server.listen()
تست timeout میخوره یا fetch واقعی میزنه.
۲. handler برای URL اشتباه
/api/todos vs http://localhost:3000/api/todos. معمولاً path نسبی کافیه.
۳. فراموش کردن resetHandlers
تست قبلی handler عوض کرده، تست بعدی fail میشه.
۴. async و findBy
بعد از fetch همیشه await screen.findByText(...) بزن، نه getBy فوری.
چکلیست Todo + MSW
- MSW server در setupTests listen/close میشه؟
- هر endpoint تستشده handler داره؟
-
userEventبا await؟ - بعد از هر تست handlers reset میشن؟
- loading state تست شده؟
ارتباط با بخشهای قبل
| بخش | نقش در Todo |
|---|---|
| ۵ | Testing Library، render، screen |
| ۶ | اگر MSW نبود، vi.fn روی fetch |
| ۷ | MSW + پروژه کامل |
TDD چیه؟ یادآوری ساده
چرخه Red-Green-Refactor اینه:
- قرمز: تست بنویس، fail ببین
- سبز: کمترین کد برای pass
- Refactor: تمیز کن، تستها سبز بمونن
- تکرار برای feature بعدی
تست fail → کد minimal → refactor → feature بعدی
Todo List دقیقاً همین مسیر رو رفت: اول لیست، بعد add، بعد complete، بعد delete.
package.json: اسکریپتهای تست
{
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest",
"test:watch": "vitest --watch",
"coverage": "vitest run --coverage"
}
}
npm run test -- --watch موقع توسعه خیلی کمک میکنه. هر ذخیره، تست دوباره اجرا میشه.
Dashboard: دو API با MSW
حالا یه مثال Dashboard میزنم: کامپوننتی که دو تا fetch میزنه.
export function Dashboard() {
const [user, setUser] = useState(null);
const [notifications, setNotifications] = useState([]);
useEffect(() => {
fetch('/api/user').then((res) => res.json()).then(setUser);
fetch('/api/notifications').then((res) => res.json()).then(setNotifications);
}, []);
if (!user || notifications.length === 0) return <div>Loading…</div>;
return (
<div>
<h1>Welcome, {user.name}</h1>
<ul>
{notifications.map((n) => (
<li key={n.id}>{n.message}</li>
))}
</ul>
</div>
);
}
handlers
export const handlers = [
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.json({ id: '123', name: 'Alice' }));
}),
rest.get('/api/notifications', (req, res, ctx) => {
return res(ctx.json([
{ id: '1', message: 'Notification 1' },
{ id: '2', message: 'Notification 2' },
]));
}),
];
تست
test('renders user and notifications', async () => {
render(<Dashboard />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
expect(await screen.findByText(/welcome, alice/i)).toBeInTheDocument();
const items = await screen.findAllByRole('listitem');
expect(items).toHaveLength(2);
});
دو endpoint، یک setup MSW. بدون mock دستی در هر تست.
تست خطا: UserProfile
test('handles server error', async () => {
server.use(
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.status(500));
}),
);
render(<UserProfile />);
expect(await screen.findByText(/failed to load user/i)).toBeInTheDocument();
});
کامپوننت باید پیام خطا نشون بده. اگر نداره، اول اون رو با TDD اضافه کن.
چک کردن body درخواست POST
گاهی میخوای ببینی فرم درست submit شده:
test('submits form data correctly', async () => {
let requestBody;
server.use(
rest.post('/api/submit', async (req, res, ctx) => {
requestBody = await req.json();
return res(ctx.status(200));
}),
);
// render + submit form ...
expect(requestBody).toEqual({ name: 'Alice', age: 30 });
});
MSW فقط جواب نمیده. میتونی درخواست ورودی رو هم چک کنی.
onUnhandledRequest: سه حالت
| مقدار | رفتار |
|---|---|
bypass |
پیشفرض: درخواست بدون handler رد میشه |
warn |
warning در console |
error |
تست fail؛ برای تست توصیه میشه |
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
یعنی اگر تستی به APIای زد که handler نداریم، سریع میفهمیم.
reset کردن state در handlers
برای handlers مرکزی با let todos:
export function resetTodos() {
todos = [
{ id: 1, title: 'Buy groceries', completed: false },
{ id: 2, title: 'Walk the dog', completed: true },
];
}
// setupTests.js
beforeEach(() => resetTodos());
هر تست از لیست تمیز شروع میکنه.
فایل تست کامل ToDoList: ساختار
// src/tests/ToDoList.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ToDoList } from '../components/ToDoList';
const mockTodos = [
{ id: 1, title: 'Buy groceries', completed: false },
{ id: 2, title: 'Walk the dog', completed: true },
];
const server = setupServer(
rest.get('/api/todos', (req, res, ctx) => res(ctx.json(mockTodos))),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('renders to-do items fetched from API', async () => { /* ... */ });
test('adds a new to-do item', async () => { /* ... */ });
test('marks a to-do item as completed', async () => { /* ... */ });
test('deletes a to-do item', async () => { /* ... */ });
چهار تست، چهار feature. هر کدوم مستقل با server.use برای override.
تمرینهای اضافی
بعد از Todo پایه اینا رو هم میتونی بزنی:
- فیلتر: all / active / completed
- ویرایش inline: rename task
- خطا: fail برای GET/POST/PUT/DELETE
- Optimistic update: UI قبل از جواب سرور
- localStorage: ذخیره محلی
- Accessibility: keyboard و screen reader
- جستجو: filter by text
- Bulk actions: complete/delete چندتایی
همه با همون الگو: اول تست، بعد کد.
MSW در browser (dev)
MSW فقط برای تست نیست. میتونی توی development هم از همون handlers استفاده کنی، یعنی frontend بدون backend واقعی کار کنه. setup جدا داره (msw/browser)، ولی ایده یکیه: یک جا mock، همه جا استفاده.
مقایسه سه لایه mock API
| لایه | ابزار | کی؟ |
|---|---|---|
| تابع | vi.fn روی fetch |
unit کوچک |
| شبکه | MSW | کامپوننت React |
| مرورگر واقعی | Playwright (بخش ۴) | E2E |
هر لایه جای خودش رو داره. همه رو با هم نزن.
پرسشهای متداول MSW
MSW v1 و v2 فرق داره؟
این مثالها از rest از msw استفاده میکنه (v1). در v2 API عوض شده، ولی ایده یکیه.
چرا تست timeout میخوره؟
معمولاً server.listen() فراموش شده یا URL handler اشتباهه.
چرا getBy fail میکنه ولی findBy کار میکنه؟
fetch async هست. باید صبر کنی UI آپدیت بشه.
جمعبندی بخش ۷
| موضوع | یاد گرفتیم |
|---|---|
| MSW | mock در سطح شبکه |
| setup | handlers + server + setupTests |
| TDD | Red-Green-Refactor روی Todo |
| CRUD | GET, POST, PUT, DELETE |
| خطا | server.use با status 500 |
| بهتر از fetch mock | endpoint زیاد = MSW |
چیزایی که توی بخش ۸ میآد
آخرین مقاله روی Snapshot testing، Code coverage، و CI با GitHub Actions میره. جمعبندی دوره اونجاست؛ اول این مثالهای تکمیلی رو هم ببین.
Login با MSW: مثال دوم
علاوه بر /api/user، یه handler برای login هم میذارم:
rest.post('/api/login', async (req, res, ctx) => {
const { username } = await req.json();
return res(ctx.status(200), ctx.json({ message: `Welcome, ${username}!` }));
}),
کامپوننت LoginForm: ساده
export function LoginForm() {
const [username, setUsername] = useState('');
const [message, setMessage] = useState('');
async function handleSubmit(e) {
e.preventDefault();
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username }),
});
const data = await res.json();
setMessage(data.message);
}
return (
<form onSubmit={handleSubmit}>
<input
aria-label="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button type="submit">Login</button>
{message && <p>{message}</p>}
</form>
);
}
تست
test('shows welcome message after login', async () => {
const user = userEvent.setup();
render(<LoginForm />);
await user.type(screen.getByLabelText('Username'), 'Steve');
await user.click(screen.getByRole('button', { name: /login/i }));
expect(await screen.findByText('Welcome, Steve!')).toBeInTheDocument();
});
بدون mock دستی؛ handler مرکزی جواب میده.
پیادهسازی error state در ToDoList
برای تست خطای API، اول کامپوننت باید error نشون بده:
const [error, setError] = useState(null);
function fetchTodos() {
fetch('/api/todos')
.then((res) => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then((data) => {
setTodos(data);
setLoading(false);
})
.catch(() => {
setError('Error loading todos');
setLoading(false);
});
}
if (error) return <div>{error}</div>;
حالا تست shows error when API fails pass میشه.
refactor: todoService.js
وقتی fetch زیاد شد، پیشنهاد میکنم API رو جدا کنی:
// todoService.js
export function getTodos() {
return fetch('/api/todos').then((res) => res.json());
}
export function createTodo(title) {
return fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
}
export function updateTodo(todo) {
return fetch(`/api/todos/${todo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(todo),
});
}
export function deleteTodo(id) {
return fetch(`/api/todos/${id}`, { method: 'DELETE' });
}
کامپوننت فقط service رو صدا میزنه. تست UI همچنان با MSW کار میکنه.
waitFor vs findBy
// findBy: ترجیح برای async
const item = await screen.findByText('Buy groceries');
// waitFor: وقتی assertion پیچیدهتره
await waitFor(() => {
expect(screen.getByText('Buy groceries')).toBeInTheDocument();
});
برای اکثر caseها findBy کافیه و خواناتره.
تست loading → loaded
test('shows loading then items', async () => {
render(<ToDoList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await screen.findByText('Buy groceries');
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
دو state داریم، loading و loaded، و هر دو تست شدن.
GraphQL با MSW
MSW فقط REST نیست؛ GraphQL هم support میکنه. این مقاله روی REST تمرکز داره، ولی ایده یکیه: handler مرکزی، override در تست.
debug وقتی MSW کار نمیکنه
server.listen()صدا زده شده؟- URL handler دقیقاً همون path که fetch میزنه؟
onUnhandledRequest: 'error'فعاله؟ پیام واضحتر میدهresetHandlersدرafterEach؟
Red-Green-Refactor: یک دور کامل
| مرحله | Todo List |
|---|---|
| Red | تست list: fail (کامپوننت نیست) |
| Green | ToDoList با fetch |
| Refactor | extract fetchTodos |
| Red | تست add: fail (input نیست) |
| Green | input + button + POST |
| ... | complete، delete |
هر feature یک چرخه. scope کوچک نگه دار.
پایان بخش ۷
MSW با TDD و Todo List همون الگویه که تو پروژه واقعی هم میتونی تکرار کنی. تمرینهای اضافی (فیلتر، جستجو) رو خودت با همین الگو بزن.
بخش ۸ آخرین قسمته: Snapshot، coverage و CI.