فارسی

یاد بگیرید چگونه از توابع mock در استراتژی تست خود برای توسعه نرم‌افزار قوی و قابل اعتماد استفاده کنید. این راهنما زمان، دلیل و نحوه پیاده‌سازی mockها را با مثال‌های عملی پوشش می‌دهد.

توابع Mock: راهنمای جامع برای توسعه‌دهندگان

در دنیای توسعه نرم‌افزار، نوشتن کدهای قوی و قابل اعتماد از اهمیت بالایی برخوردار است. تست کامل برای دستیابی به این هدف بسیار حیاتی است. به طور خاص، تست واحد (Unit testing) بر روی تست کامپوننت‌ها یا توابع به صورت ایزوله تمرکز دارد. با این حال، برنامه‌های کاربردی در دنیای واقعی اغلب شامل وابستگی‌های پیچیده‌ای هستند که تست واحدها به صورت کاملاً ایزوله را چالش‌برانگیز می‌کند. اینجاست که توابع mock وارد عمل می‌شوند.

توابع Mock چه هستند؟

یک تابع mock نسخه‌ای شبیه‌سازی شده از یک تابع واقعی است که می‌توانید در تست‌های خود از آن استفاده کنید. به جای اجرای منطق واقعی تابع، یک تابع mock به شما این امکان را می‌دهد که رفتار آن را کنترل کنید، نحوه فراخوانی آن را مشاهده کرده و مقادیر بازگشتی آن را تعریف کنید. آنها نوعی بدل تست (test double) هستند.

این‌طور در نظر بگیرید: تصور کنید در حال تست موتور یک خودرو (واحد تحت تست) هستید. موتور به قطعات مختلف دیگری مانند سیستم تزریق سوخت و سیستم خنک‌کننده وابسته است. به جای اجرای سیستم‌های واقعی تزریق سوخت و خنک‌کننده در حین تست موتور، می‌توانید از سیستم‌های mock استفاده کنید که رفتار آنها را شبیه‌سازی می‌کنند. این به شما امکان می‌دهد تا موتور را ایزوله کرده و به طور خاص بر روی عملکرد آن تمرکز کنید.

توابع Mock ابزارهای قدرتمندی برای موارد زیر هستند:

چه زمانی از توابع Mock استفاده کنیم؟

Mockها در این شرایط بیشترین کاربرد را دارند:

۱. ایزوله‌سازی واحدها با وابستگی‌های خارجی

هنگامی که واحد تحت تست شما به سرویس‌های خارجی، پایگاه‌های داده، APIها یا سایر کامپوننت‌ها وابسته است، استفاده از وابستگی‌های واقعی در حین تست می‌تواند مشکلات متعددی ایجاد کند:

مثال: تصور کنید در حال تست تابعی هستید که داده‌های کاربر را از یک API راه دور بازیابی می‌کند. به جای برقراری تماس‌های واقعی API در حین تست، می‌توانید از یک تابع mock برای شبیه‌سازی پاسخ API استفاده کنید. این به شما امکان می‌دهد تا منطق تابع را بدون اتکا به در دسترس بودن یا عملکرد API خارجی تست کنید. این امر به ویژه زمانی اهمیت دارد که API دارای محدودیت نرخ (rate limits) یا هزینه‌های مرتبط با هر درخواست باشد.

۲. تست تعاملات پیچیده

در برخی موارد، واحد تحت تست شما ممکن است به روش‌های پیچیده‌ای با سایر کامپوننت‌ها تعامل داشته باشد. توابع mock به شما امکان می‌دهند این تعاملات را مشاهده و تأیید کنید.

مثال: تابعی را در نظر بگیرید که تراکنش‌های پرداخت را پردازش می‌کند. این تابع ممکن است با یک درگاه پرداخت، یک پایگاه داده و یک سرویس اطلاع‌رسانی تعامل داشته باشد. با استفاده از توابع mock، می‌توانید تأیید کنید که تابع، درگاه پرداخت را با جزئیات صحیح تراکنش فراخوانی می‌کند، وضعیت تراکنش را در پایگاه داده به‌روزرسانی می‌کند و یک اعلان برای کاربر ارسال می‌کند.

۳. شبیه‌سازی شرایط خطا

تست مدیریت خطا برای اطمینان از استحکام برنامه شما بسیار حیاتی است. توابع mock شبیه‌سازی شرایط خطایی را که بازتولید آنها در یک محیط واقعی دشوار یا غیرممکن است، آسان می‌کنند.

مثال: فرض کنید در حال تست تابعی هستید که فایل‌ها را در یک سرویس ذخیره‌سازی ابری آپلود می‌کند. می‌توانید از یک تابع mock برای شبیه‌سازی خطای شبکه در حین فرآیند آپلود استفاده کنید. این به شما امکان می‌دهد تأیید کنید که تابع به درستی خطا را مدیریت کرده، آپلود را دوباره تلاش می‌کند یا به کاربر اطلاع می‌دهد.

۴. تست کدهای ناهمگام (Asynchronous)

کدهای ناهمگام، مانند کدهایی که از callbackها، promiseها یا async/await استفاده می‌کنند، می‌توانند برای تست چالش‌برانگیز باشند. توابع mock می‌توانند به شما در کنترل زمان‌بندی و رفتار عملیات ناهمگام کمک کنند.

مثال: تصور کنید در حال تست تابعی هستید که داده‌ها را با یک درخواست ناهمگام از سرور دریافت می‌کند. می‌توانید از یک تابع mock برای شبیه‌سازی پاسخ سرور و کنترل زمان بازگشت پاسخ استفاده کنید. این به شما امکان می‌دهد تا نحوه مدیریت سناریوهای مختلف پاسخ و وقفه زمانی (timeout) توسط تابع را تست کنید.

۵. جلوگیری از عوارض جانبی ناخواسته

گاهی اوقات، فراخوانی یک تابع واقعی در حین تست می‌تواند عوارض جانبی ناخواسته‌ای داشته باشد، مانند تغییر در پایگاه داده، ارسال ایمیل یا راه‌اندازی فرآیندهای خارجی. توابع mock با اجازه دادن به شما برای جایگزینی تابع واقعی با یک شبیه‌سازی کنترل‌شده، از این عوارض جانبی جلوگیری می‌کنند.

مثال: شما در حال تست تابعی هستید که ایمیل خوشامدگویی به کاربران جدید ارسال می‌کند. با استفاده از یک سرویس ایمیل mock، می‌توانید اطمینان حاصل کنید که عملکرد ارسال ایمیل در حین اجرای مجموعه تست شما، واقعاً به کاربران واقعی ایمیل ارسال نمی‌کند. در عوض، می‌توانید تأیید کنید که تابع تلاش می‌کند ایمیل را با اطلاعات صحیح ارسال کند.

چگونه از توابع Mock استفاده کنیم؟

مراحل خاص استفاده از توابع mock به زبان برنامه‌نویسی و فریمورک تستی که استفاده می‌کنید بستگی دارد. با این حال، فرآیند کلی معمولاً شامل مراحل زیر است:

  1. شناسایی وابستگی‌ها: مشخص کنید کدام وابستگی‌های خارجی را باید mock کنید.
  2. ایجاد اشیاء Mock: اشیاء یا توابع mock را برای جایگزینی وابستگی‌های واقعی ایجاد کنید. این mockها اغلب دارای ویژگی‌هایی مانند `called`، `returnValue` و `callArguments` هستند.
  3. پیکربندی رفتار Mock: رفتار توابع mock را تعریف کنید، مانند مقادیر بازگشتی، شرایط خطا و تعداد فراخوانی.
  4. تزریق Mockها: وابستگی‌های واقعی را با اشیاء mock در واحد تحت تست خود جایگزین کنید. این کار اغلب با استفاده از تزریق وابستگی (dependency injection) انجام می‌شود.
  5. اجرای تست: تست خود را اجرا کنید و نحوه تعامل واحد تحت تست با توابع mock را مشاهده کنید.
  6. تأیید تعاملات: تأیید کنید که توابع mock با آرگومان‌ها، مقادیر بازگشتی و تعداد دفعات مورد انتظار فراخوانی شده‌اند.
  7. بازیابی عملکرد اصلی: پس از تست، با حذف اشیاء mock و بازگشت به وابستگی‌های واقعی، عملکرد اصلی را بازیابی کنید. این کار به جلوگیری از عوارض جانبی بر روی سایر تست‌ها کمک می‌کند.

مثال‌هایی از توابع Mock در زبان‌های مختلف

در اینجا مثال‌هایی از استفاده از توابع mock در زبان‌های برنامه‌نویسی و فریمورک‌های تست محبوب آورده شده است:

جاوا اسکریپت با Jest

Jest یک فریمورک تست محبوب جاوا اسکریپت است که پشتیبانی داخلی از توابع mock را فراهم می‌کند.

// تابع برای تست
function fetchData(callback) {
  setTimeout(() => {
    callback('Data from server');
  }, 100);
}

// مورد تست
test('fetchData calls callback with correct data', (done) => {
  const mockCallback = jest.fn();
  fetchData(mockCallback);

  setTimeout(() => {
    expect(mockCallback).toHaveBeenCalledWith('Data from server');
    done();
  }, 200);
});

در این مثال، `jest.fn()` یک تابع mock ایجاد می‌کند که جایگزین تابع callback واقعی می‌شود. تست تأیید می‌کند که تابع mock با داده‌های صحیح با استفاده از `toHaveBeenCalledWith()` فراخوانی شده است.

مثال پیشرفته‌تر با استفاده از ماژول‌ها:

// user.js
import { getUserDataFromAPI } from './api';

export async function displayUserName(userId) {
  const userData = await getUserDataFromAPI(userId);
  return userData.name;
}

// api.js
export async function getUserDataFromAPI(userId) {
  // شبیه‌سازی فراخوانی API
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ id: userId, name: 'John Doe' });
    }, 50);
  });
}

// user.test.js
import { displayUserName } from './user';
import * as api from './api';

describe('displayUserName', () => {
  it('should display the user name', async () => {
    // تابع getUserDataFromAPI را mock می‌کنیم
    const mockGetUserData = jest.spyOn(api, 'getUserDataFromAPI');
    mockGetUserData.mockResolvedValue({ id: 123, name: 'Mocked Name' });

    const userName = await displayUserName(123);
    expect(userName).toBe('Mocked Name');

    // تابع اصلی را بازیابی می‌کنیم
    mockGetUserData.mockRestore();
  });
});

در اینجا، از `jest.spyOn` برای ایجاد یک تابع mock برای تابع `getUserDataFromAPI` که از ماژول `./api` وارد شده است، استفاده می‌شود. `mockResolvedValue` برای مشخص کردن مقدار بازگشتی mock استفاده می‌شود. `mockRestore` برای اطمینان از اینکه سایر تست‌ها به طور ناخواسته از نسخه mock شده استفاده نمی‌کنند، ضروری است.

پایتون با pytest و unittest.mock

پایتون چندین کتابخانه برای mocking ارائه می‌دهد، از جمله `unittest.mock` (داخلی) و کتابخانه‌هایی مانند `pytest-mock` برای استفاده ساده‌تر با pytest.

# تابع برای تست
def get_data_from_api(url):
    # در یک سناریوی واقعی، این تابع یک فراخوانی API انجام می‌دهد
    # برای سادگی، یک فراخوانی API را شبیه‌سازی می‌کنیم
    if url == "https://example.com/api":
        return {"data": "API data"}
    else:
        return None

def process_data(url):
    data = get_data_from_api(url)
    if data:
        return data["data"]
    else:
        return "No data found"

# مورد تست با استفاده از unittest.mock
import unittest
from unittest.mock import patch

class TestProcessData(unittest.TestCase):
    @patch('__main__.get_data_from_api') # جایگزینی get_data_from_api در ماژول اصلی
    def test_process_data_success(self, mock_get_data_from_api):
        # پیکربندی mock
        mock_get_data_from_api.return_value = {"data": "Mocked data"}

        # فراخوانی تابعی که در حال تست است
        result = process_data("https://example.com/api")

        # بررسی نتیجه
        self.assertEqual(result, "Mocked data")
        mock_get_data_from_api.assert_called_once_with("https://example.com/api")

    @patch('__main__.get_data_from_api')
    def test_process_data_failure(self, mock_get_data_from_api):
        mock_get_data_from_api.return_value = None
        result = process_data("https://example.com/api")
        self.assertEqual(result, "No data found")

if __name__ == '__main__':
    unittest.main()

این مثال از `unittest.mock.patch` برای جایگزینی تابع `get_data_from_api` با یک mock استفاده می‌کند. تست، mock را برای بازگرداندن یک مقدار خاص پیکربندی کرده و سپس تأیید می‌کند که تابع `process_data` نتیجه مورد انتظار را برمی‌گرداند.

در اینجا همان مثال با استفاده از `pytest-mock` آمده است:

# نسخه pytest
import pytest

def get_data_from_api(url):
    # در یک سناریوی واقعی، این تابع یک فراخوانی API انجام می‌دهد
    # برای سادگی، یک فراخوانی API را شبیه‌سازی می‌کنیم
    if url == "https://example.com/api":
        return {"data": "API data"}
    else:
        return None

def process_data(url):
    data = get_data_from_api(url)
    if data:
        return data["data"]
    else:
        return "No data found"


def test_process_data_success(mocker):
    mocker.patch('__main__.get_data_from_api', return_value={"data": "Mocked data"})
    result = process_data("https://example.com/api")
    assert result == "Mocked data"


def test_process_data_failure(mocker):
    mocker.patch('__main__.get_data_from_api', return_value=None)
    result = process_data("https://example.com/api")
    assert result == "No data found"

کتابخانه `pytest-mock` یک فیکسچر `mocker` فراهم می‌کند که ایجاد و پیکربندی mockها را در تست‌های pytest ساده می‌کند.

جاوا با Mockito

Mockito یک فریمورک محبوب mocking برای جاوا است.

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

interface DataFetcher {
    String fetchData(String url);
}

class DataProcessor {
    private final DataFetcher dataFetcher;

    public DataProcessor(DataFetcher dataFetcher) {
        this.dataFetcher = dataFetcher;
    }

    public String processData(String url) {
        String data = dataFetcher.fetchData(url);
        if (data != null) {
            return "Processed: " + data;
        } else {
            return "No data";
        }
    }
}

public class DataProcessorTest {

    @Test
    public void testProcessDataSuccess() {
        // یک DataFetcher از نوع mock ایجاد می‌کنیم
        DataFetcher mockDataFetcher = mock(DataFetcher.class);

        // mock را پیکربندی می‌کنیم
        when(mockDataFetcher.fetchData("https://example.com/api")).thenReturn("API Data");

        // DataProcessor را با mock ایجاد می‌کنیم
        DataProcessor dataProcessor = new DataProcessor(mockDataFetcher);

        // تابعی که در حال تست است را فراخوانی می‌کنیم
        String result = dataProcessor.processData("https://example.com/api");

        // نتیجه را بررسی می‌کنیم
        assertEquals("Processed: API Data", result);

        // تأیید می‌کنیم که mock فراخوانی شده است
        verify(mockDataFetcher).fetchData("https://example.com/api");
    }

    @Test
    public void testProcessDataFailure() {
        DataFetcher mockDataFetcher = mock(DataFetcher.class);
        when(mockDataFetcher.fetchData("https://example.com/api")).thenReturn(null);

        DataProcessor dataProcessor = new DataProcessor(mockDataFetcher);
        String result = dataProcessor.processData("https://example.com/api");
        assertEquals("No data", result);
        verify(mockDataFetcher).fetchData("https://example.com/api");
    }
}

در این مثال، `Mockito.mock()` یک شیء mock برای اینترفیس `DataFetcher` ایجاد می‌کند. `when()` برای پیکربندی مقدار بازگشتی mock استفاده می‌شود و `verify()` برای تأیید اینکه mock با آرگومان‌های مورد انتظار فراخوانی شده است، به کار می‌رود.

بهترین شیوه‌ها برای استفاده از توابع Mock

جایگزین‌های توابع Mock

در حالی که توابع mock ابزار قدرتمندی هستند، همیشه بهترین راه‌حل نیستند. در برخی موارد، تکنیک‌های دیگر ممکن است مناسب‌تر باشند:

نتیجه‌گیری

توابع mock ابزاری ضروری برای نوشتن تست‌های واحد مؤثر هستند که به شما امکان ایزوله کردن واحدها، کنترل رفتار، شبیه‌سازی شرایط خطا و تست کدهای ناهمگام را می‌دهند. با پیروی از بهترین شیوه‌ها و درک جایگزین‌ها، می‌توانید از توابع mock برای ساختن نرم‌افزاری قوی‌تر، قابل اعتمادتر و قابل نگهداری‌تر بهره ببرید. به یاد داشته باشید که معاوضه‌ها را در نظر بگیرید و تکنیک تست مناسب را برای هر موقعیت انتخاب کنید تا یک استراتژی تست جامع و مؤثر ایجاد کنید، صرف نظر از اینکه در کجای جهان در حال ساخت آن هستید.