تست‌نویسی در javascript/typescript — بخش ۶: Mock و Test Doubles

تست‌نویسی در 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 می‌کنیم؟

  1. ایزوله کردن — فقط کد خودمون رو تست می‌کنیم
  2. کنترل — می‌تونیم خطا یا داده خاص شبیه‌سازی کنیم
  3. سرعت — بدون شبکه و دیتابیس واقعی
  4. پیش‌بینی‌پذیری — هر بار نتیجه یکسان

به عنوان یه 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

  1. Spy روی console
  2. Stub روی fetch
  3. Fake Timers
  4. vi.mock
  5. MSW (بخش ۷)

جمع‌بندی بخش ۶

از یاد گرفتیم:

  1. Test Double — جایگزین dependency
  2. Stub — جواب ثابت
  3. Spy — رصد فراخوانی
  4. Mock — stub + spy
  5. پاکسازی — clear / reset / restore
  6. آرگومان‌هاtoHaveBeenCalledWith
  7. vi.mock — کل ماژول
  8. fetch و زمان و env — کنترل محیط
  9. DI — راه جایگزین mock
  10. localStorage — با jsdom
  11. 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.

تا بخش ۷!