تستنویسی در javascript/typescript — بخش ۶: Mock و Test Doubles
تا اینجا یاد گرفتیم unit test بنویسیم، DOM تست کنیم، و با Playwright E2E بزنیم. حالا یه موضوع مهم مونده: وقتی کد ما به چیزهای بیرونی وابسته است چی کار کنیم؟
مثلاً کد شما fetch میزنه به API، یا به localStorage دسترسی داره، یا console.log صدا میزنه. توی unit test نمیخوایم هر بار API واقعی صدا بزنیم — کنده، ناپایدار، و گاهی پولیه!
راهحل در تستنویسی: Test Doubles — جایگزینهای کنترلشده برای dependencyهای واقعی.
خب بزن بریم — ساده و قدمبهقدم!
Test Double چیه؟
Test Double یه اسم کلیه برای هر چیزی که جای dependency واقعی توی تست میذاریم.
سه نوع اصلی رو جدا میکنه:
| نوع | کارش چیه؟ | مثال ساده |
|---|---|---|
| Stub | جواب از پیش تعیینشده برمیگردونه | «همیشه بگو API موفق بود» |
| Spy | فقط تماشا میکنه چی صدا زده شد | «آیا console.error صدا زده شد؟» |
| Mock | هم جواب میده، هم ثبت میکنه چند بار صدا زده شد | stub + spy با هم |
چرا اصلاً mock میکنیم؟
- ایزوله کردن — فقط کد خودمون رو تست میکنیم
- کنترل — میتونیم خطا یا داده خاص شبیهسازی کنیم
- سرعت — بدون شبکه و دیتابیس واقعی
- پیشبینیپذیری — هر بار نتیجه یکسان
به عنوان یه rule of thumb: کدی که کنترلش نداری (API شخص ثالث، شبکه) رو mock کن. کد خودت رو واقعی تست کن.
Stub — جواب ثابت بده
Stub یه تابع جایگزینه که همیشه (یا طبق تنظیم ما) یه جواب مشخص برمیگردونه. معمولاً نگران «چند بار صدا زده شد» نیستیم — فقط میخوایم رفتار قابل پیشبینی داشته باشیم.
مثال: stub کردن fetch
// concerts.js
export async function getConcertDetails(band) {
const response = await fetch(`/api/concerts?band=${band}`);
const data = await response.json();
return data;
}
// concerts.test.js
import { describe, it, expect, vi } from 'vitest';
import { getConcertDetails } from './concerts';
describe('getConcertDetails', () => {
it('returns concert details from the API', async () => {
const fetchStub = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ band: 'Green Day', venue: 'Madison Square Garden' }),
}),
);
global.fetch = fetchStub;
const result = await getConcertDetails('Green Day');
expect(result).toEqual({ band: 'Green Day', venue: 'Madison Square Garden' });
fetchStub.mockRestore();
});
});
خلاصه: به جای API واقعی، fetchStub جواب ثابت میده. تست سریع و بدون اینترنت!
کی از Stub استفاده کنیم؟
- وقتی فقط خروجی مهمه، نه اینکه چند بار صدا زده شد
- وقتی میخوایم خطا یا timeout شبیهسازی کنیم
- وقتی I/O واقعی نمیخوایم (فایل، شبکه)
Spy — فقط نگاه کن چی شد
Spy روی تابع واقعی «گوش میذاره». معمولاً خود تابع همون کار قبلی رو میکنه — فقط ما میبینیم با چه آرگومانهایی صدا زده شد.
ساخت Spy با vi.spyOn
const spy = vi.spyOn(console, 'log');
مثال: لاگ خطا
function handleTicketSaleError(errorMessage) {
console.error(`Error: ${errorMessage}`);
}
describe('handleTicketSaleError', () => {
it('logs an error message when ticket sale fails', () => {
const errorSpy = vi.spyOn(console, 'error');
handleTicketSaleError('Payment declined');
expect(errorSpy).toHaveBeenCalledWith('Error: Payment declined');
errorSpy.mockRestore();
});
});
assertionهای مفید برای Spy
expect(spy).toHaveBeenCalled(); // حداقل یک بار
expect(spy).toHaveBeenCalledTimes(1); // دقیقاً چند بار
expect(spy).toHaveBeenCalledWith('arg'); // با چه آرگومانی
expect(spy).toHaveBeenLastCalledWith('x'); // آخرین بار
مثال: spy روی تابع خودمون
import * as math from './arithmetic';
test('calls add with correct arguments', () => {
const addSpy = vi.spyOn(math, 'add');
const result = math.add(2, 3);
expect(addSpy).toHaveBeenCalledWith(2, 3);
expect(result).toBe(5);
addSpy.mockRestore();
});
نکته مهم: بعد از تست حتماً
mockRestore()بزنید — وگرنه تستهای بعدی خراب میشن!
Mock — stub و spy با هم
نکته: معمولاً تست کدی که کنترلش نداری مفید نیست. Mock یعنی هم رفتار رو تعریف میکنیم، هم ثبت میکنیم چطور صدا زده شد.
ساخت Mock با vi.fn()
const getNumber = vi.fn(() => 5000);
const number = getNumber();
expect(number).toBe(5000);
expect(getNumber).toHaveBeenCalled();
متدهای پرکاربرد Mock
const paymentMock = vi.fn();
// جوابهای مختلف در هر بار صدا
paymentMock
.mockReturnValueOnce('Payment Successful')
.mockReturnValueOnce('Payment Failed');
expect(paymentMock()).toBe('Payment Successful');
expect(paymentMock()).toBe('Payment Failed');
expect(paymentMock).toHaveBeenCalledTimes(2);
| متد | کار |
|---|---|
mockReturnValue(val) |
همیشه این رو برگردون |
mockReturnValueOnce(val) |
فقط بار بعد |
mockResolvedValue(val) |
برای Promise موفق |
mockRejectedValue(err) |
برای Promise ناموفق |
mockImplementation(fn) |
کل منطق رو خودت بنویس |
مثال mock کردن fetch (با ثبت فراخوانی)
describe('getConcertDetails', () => {
it('returns concert details and tracks fetch call', async () => {
const fetchMock = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ band: 'Green Day', venue: 'Madison Square Garden' }),
}),
);
global.fetch = fetchMock;
const result = await getConcertDetails('Green Day');
expect(result).toEqual({ band: 'Green Day', venue: 'Madison Square Garden' });
expect(fetchMock).toHaveBeenCalledTimes(1);
fetchMock.mockRestore();
});
});
پاکسازی Mock — clear، reset، restore
mockها اگر جمع نشن، تستهای بعدی رو خراب میکنن. سه سطح داریم:
| عمل | چی پاک میشه؟ |
|---|---|
clear (mockClear) |
فقط تاریخچه فراخوانی |
reset (mockReset) |
تاریخچه + implementation سفارشی |
restore (mockRestore) |
همه چیز — برمیگرده به تابع اصلی |
const myFunc = vi.fn(() => 'mocked');
myFunc('arg');
myFunc.mockClear(); // تعداد صداها = 0، ولی هنوز 'mocked' برمیگردونه
myFunc.mockReset(); // مثل تازه ساخته شده
vi.spyOn(Math, 'random').mockReturnValue(0.5);
Math.random.mockRestore(); // Math.random دوباره واقعی میشه
در سطح کل پروژه
afterEach(() => {
vi.clearAllMocks(); // تاریخچه همه mockها
// vi.resetAllMocks();
// vi.restoreAllMocks();
});
rule of thumb: توی
afterEachحداقلvi.clearAllMocks()یاvi.restoreAllMocks()بذارید.
چک کردن آرگومانهای تابع
گاهی مهمتر از خروجی، اینه که تابع با چه دادهای صدا زده شده.
const mockFn = vi.fn();
mockFn('Green Day', 'American Idiot');
expect(mockFn).toHaveBeenCalledWith('Green Day', 'American Idiot');
چند بار صدا با آرگومانهای مختلف
mockFn('Green Day', 'Dookie');
mockFn('Nirvana', 'Nevermind');
expect(mockFn).toHaveBeenNthCalledWith(1, 'Green Day', 'Dookie');
expect(mockFn).toHaveBeenNthCalledWith(2, 'Nirvana', 'Nevermind');
دسترسی دستی: mock.calls
const firstCallArgs = mockFn.mock.calls[0];
expect(firstCallArgs).toEqual(['Green Day', 'Dookie']);
callback در کد async
async function fetchBandData(bandName, callback) {
await new Promise((resolve) => setTimeout(resolve, 100));
callback(`${bandName} data`);
}
it('should call the callback with the correct data', async () => {
const callback = vi.fn();
await fetchBandData('Green Day', callback);
expect(callback).toHaveBeenCalledWith('Green Day data');
});
Mock کردن ماژولها — vi.mock
وقتی کل فایل یا سرویس رو میخوای جایگزین کنی:
vi.mock('./send-to-server', {
sendToServer: vi.fn(),
});
یا سادهتر — auto-mock:
vi.mock('./send-to-server');
مثال log در development vs production
// log.js — سادهشده
export function log(message) {
if (import.meta.env.MODE === 'development') {
console.log(message);
} else {
sendToServer('info', message);
}
}
import { sendToServer } from './send-to-server';
vi.mock('./send-to-server', { sendToServer: vi.fn() });
describe('production', () => {
beforeEach(() => {
vi.stubEnv('MODE', 'production');
});
afterEach(() => {
vi.unstubAllEnvs();
vi.resetAllMocks();
});
it('sends messages to the server in production mode', () => {
log('Hello, world!');
expect(sendToServer).toHaveBeenCalledWith('info', 'Hello, world!');
});
});
Mock کردن fetch
// api.js
async function getData() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
return response.json();
}
export { getData };
import { test, vi, expect, afterEach } from 'vitest';
import { getData } from './api';
afterEach(() => {
vi.clearAllMocks();
});
test('fetches data successfully', async () => {
const mockResponse = { userId: 1, id: 1, title: 'Test Todo', completed: false };
global.fetch = vi.fn(() =>
Promise.resolve({ json: () => Promise.resolve(mockResponse) }),
);
const data = await getData();
expect(data).toEqual(mockResponse);
expect(fetch).toHaveBeenCalledTimes(1);
});
test('handles fetch failure', async () => {
global.fetch = vi.fn(() => Promise.reject('API is down'));
await expect(getData()).rejects.toEqual('API is down');
});
هدف تست fetch نیست — هدف اینه که کد شما وقتی fetch موفق یا ناموفق شد درست رفتار کنه.
Mock کردن زمان — vi.useFakeTimers
برای setTimeout، setInterval، Date:
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
function delay(callback) {
setTimeout(() => callback('Delayed'), 1000);
}
it('should call callback after delay', () => {
const callback = vi.fn();
delay(callback);
vi.advanceTimersByTime(1000); // ۱ ثانیه جلو برو — بدون صبر واقعی!
expect(callback).toHaveBeenCalledWith('Delayed');
});
تنظیم تاریخ
const date = new Date(2012, 1, 1, 13);
vi.setSystemTime(date);
expect(new Date().getFullYear()).toBe(2012);
متدهای مفید: vi.advanceTimersByTime(ms)، vi.runAllTimers()، vi.clearAllTimers().
Mock کردن Environment Variables
describe('development', () => {
beforeEach(() => {
vi.stubEnv('MODE', 'development');
});
afterEach(() => {
vi.restoreAllMocks();
});
it('logs to the console in development mode', () => {
const spy = vi.spyOn(console, 'log');
log('Hello, world!');
expect(spy).toHaveBeenCalledWith('Hello, world!');
});
});
describe('production', () => {
beforeEach(() => {
vi.stubEnv('MODE', 'production');
});
it('does not log to the console in production mode', () => {
const spy = vi.spyOn(console, 'log');
log('Hello, world!');
expect(spy).not.toHaveBeenCalled();
});
});
راه دیگر: Dependency Injection
گاهی mock لازم نیست — وابستگی رو از بیرون میدی.
بدون DI (سخت برای تست)
class UserService {
getUser() {
const apiClient = new ApiClient(); // همیشه واقعی!
return apiClient.get('/user');
}
}
با DI (راحت برای تست)
class UserService {
constructor(apiClient) {
this.apiClient = apiClient;
}
getUser() {
return this.apiClient.get('/user');
}
}
it('fetches user data', async () => {
const fakeApiClient = {
get: vi.fn().mockResolvedValue({ id: 1, name: 'Steve' }),
};
const userService = new UserService(fakeApiClient);
const user = await userService.getUser();
expect(fakeApiClient.get).toHaveBeenCalledWith('/user');
expect(user).toEqual({ id: 1, name: 'Steve' });
});
نکته: گاهی به جای mock پیچیده، fake object از بیرون بده — تمیزتره.
وقتی mock نزنیم
برای توابع ساده و deterministic، اصلاً mock نکن:
function calculateTotal(cartItems) {
return cartItems.reduce((total, item) => total + item.price, 0);
}
test('calculates correct total', () => {
const items = [
{ name: 'Widget', price: 9.99 },
{ name: 'Gizmo', price: 12.49 },
];
expect(calculateTotal(items)).toBe(22.48);
});
mock برای همه چیز لازم نیست. زیاد mock کردن تستها رو شکننده میکنه.
خطاهای رایج
۱. فراموش کردن mockRestore
تست A روی console.log spy میذاره، restore نمیکنه — تست B عجیب fail میشه.
۲. mock ماندگار بین تستها
همیشه afterEach با clearAllMocks یا restoreAllMocks.
۳. mock کردن چیزهایی که لازم نیست
اگر تابع pure و ساده است — واقعی تستش کن.
۴. تست implementation به جای رفتار
مهم اینه کاربر چه میبینه، نه اینکه داخلاً کدوم تابع private صدا زده شد.
خلاصه
| ابزار | یک خط |
|---|---|
| Stub | جواب ثابت بده |
| Spy | ببین چی صدا زده شد |
| Mock | هر دو |
vi.mock |
کل ماژول عوض کن |
vi.useFakeTimers |
زمان رو کنترل کن |
vi.stubEnv |
env عوض کن |
| DI | وابستگی از بیرون بده |
تیزر بخش ۷: MSW و Task List
توی مقاله بعدی با Mock Service Worker (MSW) API رو در سطح شبکه mock میکنیم — بدون دستکاری global.fetch در هر تست. بعد یه Todo List کامل با React میسازیم (مثال ).
موفق باشید!
خب دوستان، mock اولش گیجکنندهست — ولی با تمرین جا میافته. توی بخش بعدی MSW رو میبینیم که کار تمیزتره. تا بعد!
جدول مقایسه کامل Stub / Spy / Mock
| Stub | Spy | Mock | |
|---|---|---|---|
| رفتار عوض میکنه؟ | بله | معمولاً نه | بله |
| فراخوانیها رو ثبت میکنه؟ | نه | بله | بله |
| ابزار Vitest | vi.fn(() => value) |
vi.spyOn(obj, 'method') |
vi.fn() + mockReturnValue |
| مثال | fetch همیشه موفق | console.error صدا زده شد؟ | payment دو بار با جواب مختلف |
وقتی گیج شدید: فقط میخوای جواب ثابت؟ → Stub. فقط میخوای ببینی صدا زده شد؟ → Spy. هر دو؟ → Mock.
تمرین ذهنی: کدوم ابزار؟
سوال ۱: تابعی دارید که sendEmail صدا میزنه. میخواید مطمئن بشید با ایمیل درست صدا زده شده — ولی واقعاً ایمیل نفرستید.
→ Spy روی sendEmail (یا mock با vi.fn() و چک toHaveBeenCalledWith)
سوال ۲: تابعی دارید که نتیجه fetch رو پردازش میکنه. فقط میخواید بدون شبکه تست کنید.
→ Stub روی fetch با جواب JSON ثابت
سوال ۳: میخواید هم جواب API رو کنترل کنید، هم مطمئن بشید دقیقاً یک بار fetch زده شده.
→ Mock
مثال دوم Stub — notify
:
// notifier.js
export function notify(message) {
// در واقع ایمیل یا SMS میفرسته
}
import { test, expect, vi } from 'vitest';
test('sends a notification', () => {
const notifyStub = vi.fn();
notifyStub('Test message');
expect(notifyStub).toHaveBeenCalledWith('Test message');
});
اینجا notifyStub جای تابع واقعی نشسته — فقط ثبت میکنه با چه پیامی «صدا» زده شد.
Spy روی console — الگوی رایج
خیلی تستها فقط میخوان ببینن آیا لاگ یا خطا درست چاپ شده:
function logInfo(msg) {
console.log(`[INFO] ${msg}`);
}
function logError(msg) {
console.error(`[ERROR] ${msg}`);
}
describe('logging', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('logs info correctly', () => {
const logSpy = vi.spyOn(console, 'log');
logInfo('Server started');
expect(logSpy).toHaveBeenCalledWith('[INFO] Server started');
});
it('logs error correctly', () => {
const errorSpy = vi.spyOn(console, 'error');
logError('Connection failed');
expect(errorSpy).toHaveBeenCalledWith('[ERROR] Connection failed');
});
});
mockImplementation — وقتی منطق لازم داری
const calculator = vi.fn((a, b) => a + b);
expect(calculator(2, 3)).toBe(5);
expect(calculator).toHaveBeenCalledWith(2, 3);
برای Promise:
const fetchUser = vi.fn().mockResolvedValue({ id: 1, name: 'Ali' });
const user = await fetchUser();
expect(user.name).toBe('Ali');
برای خطا:
const fetchUser = vi.fn().mockRejectedValue(new Error('Network error'));
await expect(fetchUser()).rejects.toThrow('Network error');
vi.mock — mock کردن کل ماژول (جزئیات)
فرض کنید api.js چند export داره:
// api.js
export function fetchUser(id) { /* ... */ }
export function fetchPosts(userId) { /* ... */ }
// user.test.js
import { vi } from 'vitest';
vi.mock('./api', () => ({
fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: 'Test' })),
fetchPosts: vi.fn(() => Promise.resolve([])),
}));
import { fetchUser } from './api';
import { getUserProfile } from './user';
test('getUserProfile uses fetchUser', async () => {
const profile = await getUserProfile(1);
expect(fetchUser).toHaveBeenCalledWith(1);
expect(profile.name).toBe('Test');
});
نکته: vi.mock معمولاً بالای فایل و قبل از import ماژول تحت تست قرار میگیره (Vitest hoist میکنه).
Fake Timers — مثال addTimestamp
برای fake timer از این الگو استفاده میکنیم. الگوی کلی:
export function addTimestamp(message) {
const now = new Date().toISOString();
return `${now}: ${message}`;
}
describe('addTimestamp', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-06-15T10:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('includes a fixed timestamp', () => {
const result = addTimestamp('Hello');
expect(result).toContain('2024-06-15');
expect(result).toContain('Hello');
});
});
بدون fake timer، هر بار تست تاریخ متفاوت میگرفت — تست شکننده میشد.
Dependency Injection با تابع ساده
کلاس لازم نیست:
function getUser(apiClient) {
return apiClient.get('/user');
}
test('returns user from api', async () => {
const fakeClient = {
get: vi.fn().mockResolvedValue({ id: 1, name: 'Steve' }),
};
const user = await getUser(fakeClient);
expect(user.name).toBe('Steve');
});
function getUserCart(userId, cartService) {
const items = cartService.getCartByUserId(userId);
return calculateTotal(items);
}
test('returns correct total from cart service', () => {
const cartService = {
getCartByUserId: () => [
{ name: 'Thing', price: 19.99 },
{ name: 'Other', price: 5.99 },
],
};
expect(getUserCart(123, cartService)).toBe(25.98);
});
نکته: calculateTotal رو mock نکن — بذار واقعی اجرا بشه. فقط cartService رو fake بده.
چکلیست قبل از استفاده از Mock
- آیا واقعاً به شبکه/API واقعی نیاز داریم؟ اگر نه → mock
- آیا میتونیم با DI fake object بدیم؟ گاهی تمیزتر از
vi.mockاست - بعد از تست restore/clear کردیم؟
- داریم رفتار تست میکنیم نه جزئیات داخلی؟
پرسشهای متداول
فرق vi.fn() و vi.spyOn() چیه؟
vi.fn() تابع کاملاً جدید میسازه. vi.spyOn() روی تابع موجود گوش میذاره (یا رفتارش رو عوض میکنه).
کی global.fetch عوض کنم کی MSW؟
برای تستهای کوچک vi.fn روی fetch کافیه. وقتی API زیاد شد MSW تمیزتره — بخش ۷.
mock یعنی تست بد؟ نه. mock ابزاره. زیادهروی در mock بد است.
تست localStorage — بدون mock پیچیده
نکته: گاهی لازم نیست localStorage رو mock کنی — Vitest با jsdom خودش localStorage داره!
it('should properly assign to localStorage', () => {
const key = 'secret';
const message = "It's a secret to everybody.";
localStorage.setItem(key, message);
expect(localStorage.getItem(key)).toBe(message);
});
مثال secret-input — DOM + localStorage
export function createSecretInput() {
const id = 'secret-input';
const container = document.createElement('div');
const input = document.createElement('input');
const label = document.createElement('label');
const button = document.createElement('button');
input.id = id;
input.type = 'password';
input.placeholder = 'Enter your secret…';
label.htmlFor = id;
label.textContent = 'Secret';
button.textContent = 'Store Secret';
button.addEventListener('click', () => {
localStorage.setItem('secret', input.value);
input.value = '';
});
container.appendChild(label);
container.appendChild(input);
container.appendChild(button);
return container;
}
describe('createSecretInput', () => {
beforeEach(() => localStorage.clear());
it('should store the value in localStorage', () => {
const secretInput = createSecretInput();
const input = secretInput.querySelector('input');
const button = secretInput.querySelector('button');
input.value = 'my secret';
button.click();
expect(localStorage.getItem('secret')).toBe('my secret');
});
});
Best Practices
| قانون | توضیح ساده |
|---|---|
| کم mock کن | فقط dependency بیرونی |
| reset بین تستها | vi.clearAllMocks() در afterEach |
| Spy برای interaction | وقتی implementation واقعی مهمه |
| تست خوانا | Arrange → Act → Assert |
| restore globalها | fetch، console، Math.random |
| رفتار نه implementation | خروجی مهمتر از جزئیات داخلی |
وقتی mock لازم نیست
| روش | کی؟ |
|---|---|
| DI | وابستگی از بیرون بده |
| Fake object | object ساده با متدهای mock |
| بدون mock | تابع pure |
const fakeEmailService = { send: vi.fn().mockResolvedValue(true) };
const userService = new UserService(fakeEmailService);
auto-mock
vi.mock('./api');
import { fetchUser } from './api';
fetchUser.mockResolvedValue({ id: 1 });
سناریوی کامل: log در dev و production
// log.js
export function log(message) {
if (import.meta.env.MODE === 'development') {
console.log(message);
} else {
sendToServer('info', message);
}
}
تست development → vi.stubEnv + spy روی console.log
تست production → vi.mock('./send-to-server') + چک toHaveBeenCalledWith
Fake Timers — debounce
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
it('calls function after delay', () => {
vi.useFakeTimers();
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
debounced();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
جدول تصمیمگیری سریع
شبکه/API واقعی لازمه؟ → نه: mock fetch | بله: MSW یا E2E
فقط میخوای ببینی صدا زده شد؟ → Spy
میتونی dependency از بیرون بدی؟ → DI
زمان/تاریخ مهمه؟ → vi.useFakeTimers()
مسیر یادگیری mock
- Spy روی
console - Stub روی
fetch - Fake Timers
vi.mock- MSW (بخش ۷)
جمعبندی بخش ۶
از یاد گرفتیم:
- Test Double — جایگزین dependency
- Stub — جواب ثابت
- Spy — رصد فراخوانی
- Mock — stub + spy
- پاکسازی — clear / reset / restore
- آرگومانها —
toHaveBeenCalledWith - vi.mock — کل ماژول
- fetch و زمان و env — کنترل محیط
- DI — راه جایگزین mock
- localStorage — با jsdom
- Best practices — کم mock، reset، رفتار
بخش بعدی: MSW — راه حرفهایتر برای API mocking + پروژه Todo List.
موفق باشید!
Mock کردن DOM
گاهی لازمه متد DOM رو mock کنی. مثال counter:
describe('Counter app', () => {
it('should increment the count when the button is clicked', () => {
document.body.innerHTML = `
<p id="counter">0</p>
<button id="increment">Increment</button>
`;
const button = document.getElementById('increment');
const counter = document.getElementById('counter');
let count = 0;
button.addEventListener('click', () => {
count += 1;
counter.textContent = count;
});
button.click();
expect(counter.textContent).toBe('1');
});
});
نکته: state داخلی (count) رو تست نکن — چیزی که کاربر میبینه (textContent) رو تست کن.
spy روی getElementById
it('should call getElementById', () => {
const spy = vi.spyOn(global.document, 'getElementById').mockReturnValue({
textContent: '0',
});
const element = document.getElementById('counter');
expect(spy).toHaveBeenCalled();
expect(element.textContent).toBe('0');
spy.mockRestore();
});
سناریو: payment با mockReturnValueOnce
function processPayments(chargeFn) {
const first = chargeFn(100);
const second = chargeFn(200);
return { first, second };
}
test('handles mixed payment results', () => {
const charge = vi.fn()
.mockReturnValueOnce({ success: true })
.mockReturnValueOnce({ success: false, error: 'Declined' });
const result = processPayments(charge);
expect(result.first.success).toBe(true);
expect(result.second.error).toBe('Declined');
expect(charge).toHaveBeenCalledTimes(2);
});
mockRejectedValue — خطای async
async function loadUser(id, api) {
return api.fetchUser(id);
}
test('propagates API error', async () => {
const api = {
fetchUser: vi.fn().mockRejectedValue(new Error('Not found')),
};
await expect(loadUser(999, api)).rejects.toThrow('Not found');
});
vi.spyOn روی object method
const storage = {
get(key) { return this._data?.[key]; },
set(key, val) { this._data = { ...this._data, [key]: val }; },
_data: {},
};
test('storage set and get', () => {
const setSpy = vi.spyOn(storage, 'set');
storage.set('name', 'Ali');
expect(setSpy).toHaveBeenCalledWith('name', 'Ali');
expect(storage.get('name')).toBe('Ali');
setSpy.mockRestore();
});
تست callback — الگوی رایج
function onSuccess(callback) {
callback({ status: 'ok' });
}
test('calls callback with status', () => {
const cb = vi.fn();
onSuccess(cb);
expect(cb).toHaveBeenCalledWith({ status: 'ok' });
});
mock و async/await — یادآوری
// درست
await expect(asyncFn()).resolves.toBe('done');
// اشتباه — بدون await
expect(asyncFn()).resolves.toBe('done'); // warning یا fail
وقتی vi.mock کافی نیست — partial mock
vi.mock('./utils', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
expensiveOperation: vi.fn(() => 'mocked'),
};
});
فقط یک تابع mock — بقیه واقعی.
چکلیست نهایی mock
- فقط dependency بیرونی mock شده؟
-
afterEachبا clear/restore؟ - تست async با
await؟ - assertion روی رفتار، نه implementation؟
- اگر API زیاد شد → فکر کن MSW (بخش ۷)
ارتباط با بخشهای دیگر
| بخش | ارتباط با mock |
|---|---|
| ۲-۳ | unit test پایه |
| ۵ | DOM — گاهی spy روی console کافیه |
| ۷ | MSW جایگزین fetch mock |
| ۸ | coverage نشون میده mock شده چقدر اجرا شده |
پایان بخش ۶
mock اولش گیجکنندهست — ولی با تمرین جا میافته. از Spy ساده شروع کن، بعد fetch، بعد MSW.
تا بخش ۷!