تستنویسی در 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 و CI!