تستنویسی در 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 یعنی هم رفتار رو تعریف میکنیم، هم ثبت میکنیم چطور صدا زده شد. معمولاً همون dependencyهایی که کنترلشون نداری (مثل API بیرونی) رو اینطور جایگزین میکنی.
ساخت 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 به جای رفتار
روی چیزی assert کن که کاربر میبینه یا خروجی بیرونی تابع. اینکه کدوم تابع 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 میسازیم.
جدول مقایسه کامل 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
یه 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 ساده شروع کن، بعد stub روی fetch، بعد Fake Timers و vi.mock. توی بخش ۷ میریم سراغ MSW و یه Todo List واقعی.