یاد بگیرید چگونه از توابع mock در استراتژی تست خود برای توسعه نرمافزار قوی و قابل اعتماد استفاده کنید. این راهنما زمان، دلیل و نحوه پیادهسازی mockها را با مثالهای عملی پوشش میدهد.
توابع Mock: راهنمای جامع برای توسعهدهندگان
در دنیای توسعه نرمافزار، نوشتن کدهای قوی و قابل اعتماد از اهمیت بالایی برخوردار است. تست کامل برای دستیابی به این هدف بسیار حیاتی است. به طور خاص، تست واحد (Unit testing) بر روی تست کامپوننتها یا توابع به صورت ایزوله تمرکز دارد. با این حال، برنامههای کاربردی در دنیای واقعی اغلب شامل وابستگیهای پیچیدهای هستند که تست واحدها به صورت کاملاً ایزوله را چالشبرانگیز میکند. اینجاست که توابع mock وارد عمل میشوند.
توابع Mock چه هستند؟
یک تابع mock نسخهای شبیهسازی شده از یک تابع واقعی است که میتوانید در تستهای خود از آن استفاده کنید. به جای اجرای منطق واقعی تابع، یک تابع mock به شما این امکان را میدهد که رفتار آن را کنترل کنید، نحوه فراخوانی آن را مشاهده کرده و مقادیر بازگشتی آن را تعریف کنید. آنها نوعی بدل تست (test double) هستند.
اینطور در نظر بگیرید: تصور کنید در حال تست موتور یک خودرو (واحد تحت تست) هستید. موتور به قطعات مختلف دیگری مانند سیستم تزریق سوخت و سیستم خنککننده وابسته است. به جای اجرای سیستمهای واقعی تزریق سوخت و خنککننده در حین تست موتور، میتوانید از سیستمهای mock استفاده کنید که رفتار آنها را شبیهسازی میکنند. این به شما امکان میدهد تا موتور را ایزوله کرده و به طور خاص بر روی عملکرد آن تمرکز کنید.
توابع Mock ابزارهای قدرتمندی برای موارد زیر هستند:
- ایزولهسازی واحدها: حذف وابستگیهای خارجی برای تمرکز بر رفتار یک تابع یا کامپوننت واحد.
- کنترل رفتار: تعریف مقادیر بازگشتی خاص، ایجاد خطا، یا اجرای منطق سفارشی در حین تست.
- مشاهده تعاملات: ردیابی تعداد دفعات فراخوانی یک تابع، آرگومانهایی که دریافت میکند و ترتیب فراخوانی آن.
- شبیهسازی موارد مرزی (Edge Cases): ایجاد آسان سناریوهایی که بازتولید آنها در یک محیط واقعی دشوار یا غیرممکن است (مانند خطاهای شبکه، خطاهای پایگاه داده).
چه زمانی از توابع Mock استفاده کنیم؟
Mockها در این شرایط بیشترین کاربرد را دارند:۱. ایزولهسازی واحدها با وابستگیهای خارجی
هنگامی که واحد تحت تست شما به سرویسهای خارجی، پایگاههای داده، APIها یا سایر کامپوننتها وابسته است، استفاده از وابستگیهای واقعی در حین تست میتواند مشکلات متعددی ایجاد کند:
- کندی تستها: وابستگیهای واقعی میتوانند برای راهاندازی و اجرا کند باشند و زمان اجرای تست را به طور قابل توجهی افزایش دهند.
- عدم پایداری تستها: وابستگیهای خارجی میتوانند غیرقابل پیشبینی و مستعد خطا باشند که منجر به تستهای ناپایدار (flaky) میشود.
- پیچیدگی: مدیریت و پیکربندی وابستگیهای واقعی میتواند پیچیدگی غیرضروری به تنظیمات تست شما اضافه کند.
- هزینه: استفاده از سرویسهای خارجی اغلب هزینهبر است، به خصوص برای تستهای گسترده.
مثال: تصور کنید در حال تست تابعی هستید که دادههای کاربر را از یک API راه دور بازیابی میکند. به جای برقراری تماسهای واقعی API در حین تست، میتوانید از یک تابع mock برای شبیهسازی پاسخ API استفاده کنید. این به شما امکان میدهد تا منطق تابع را بدون اتکا به در دسترس بودن یا عملکرد API خارجی تست کنید. این امر به ویژه زمانی اهمیت دارد که API دارای محدودیت نرخ (rate limits) یا هزینههای مرتبط با هر درخواست باشد.
۲. تست تعاملات پیچیده
در برخی موارد، واحد تحت تست شما ممکن است به روشهای پیچیدهای با سایر کامپوننتها تعامل داشته باشد. توابع mock به شما امکان میدهند این تعاملات را مشاهده و تأیید کنید.
مثال: تابعی را در نظر بگیرید که تراکنشهای پرداخت را پردازش میکند. این تابع ممکن است با یک درگاه پرداخت، یک پایگاه داده و یک سرویس اطلاعرسانی تعامل داشته باشد. با استفاده از توابع mock، میتوانید تأیید کنید که تابع، درگاه پرداخت را با جزئیات صحیح تراکنش فراخوانی میکند، وضعیت تراکنش را در پایگاه داده بهروزرسانی میکند و یک اعلان برای کاربر ارسال میکند.
۳. شبیهسازی شرایط خطا
تست مدیریت خطا برای اطمینان از استحکام برنامه شما بسیار حیاتی است. توابع mock شبیهسازی شرایط خطایی را که بازتولید آنها در یک محیط واقعی دشوار یا غیرممکن است، آسان میکنند.
مثال: فرض کنید در حال تست تابعی هستید که فایلها را در یک سرویس ذخیرهسازی ابری آپلود میکند. میتوانید از یک تابع mock برای شبیهسازی خطای شبکه در حین فرآیند آپلود استفاده کنید. این به شما امکان میدهد تأیید کنید که تابع به درستی خطا را مدیریت کرده، آپلود را دوباره تلاش میکند یا به کاربر اطلاع میدهد.
۴. تست کدهای ناهمگام (Asynchronous)
کدهای ناهمگام، مانند کدهایی که از callbackها، promiseها یا async/await استفاده میکنند، میتوانند برای تست چالشبرانگیز باشند. توابع mock میتوانند به شما در کنترل زمانبندی و رفتار عملیات ناهمگام کمک کنند.
مثال: تصور کنید در حال تست تابعی هستید که دادهها را با یک درخواست ناهمگام از سرور دریافت میکند. میتوانید از یک تابع mock برای شبیهسازی پاسخ سرور و کنترل زمان بازگشت پاسخ استفاده کنید. این به شما امکان میدهد تا نحوه مدیریت سناریوهای مختلف پاسخ و وقفه زمانی (timeout) توسط تابع را تست کنید.
۵. جلوگیری از عوارض جانبی ناخواسته
گاهی اوقات، فراخوانی یک تابع واقعی در حین تست میتواند عوارض جانبی ناخواستهای داشته باشد، مانند تغییر در پایگاه داده، ارسال ایمیل یا راهاندازی فرآیندهای خارجی. توابع mock با اجازه دادن به شما برای جایگزینی تابع واقعی با یک شبیهسازی کنترلشده، از این عوارض جانبی جلوگیری میکنند.
مثال: شما در حال تست تابعی هستید که ایمیل خوشامدگویی به کاربران جدید ارسال میکند. با استفاده از یک سرویس ایمیل mock، میتوانید اطمینان حاصل کنید که عملکرد ارسال ایمیل در حین اجرای مجموعه تست شما، واقعاً به کاربران واقعی ایمیل ارسال نمیکند. در عوض، میتوانید تأیید کنید که تابع تلاش میکند ایمیل را با اطلاعات صحیح ارسال کند.
چگونه از توابع Mock استفاده کنیم؟
مراحل خاص استفاده از توابع mock به زبان برنامهنویسی و فریمورک تستی که استفاده میکنید بستگی دارد. با این حال، فرآیند کلی معمولاً شامل مراحل زیر است:
- شناسایی وابستگیها: مشخص کنید کدام وابستگیهای خارجی را باید mock کنید.
- ایجاد اشیاء Mock: اشیاء یا توابع mock را برای جایگزینی وابستگیهای واقعی ایجاد کنید. این mockها اغلب دارای ویژگیهایی مانند `called`، `returnValue` و `callArguments` هستند.
- پیکربندی رفتار Mock: رفتار توابع mock را تعریف کنید، مانند مقادیر بازگشتی، شرایط خطا و تعداد فراخوانی.
- تزریق Mockها: وابستگیهای واقعی را با اشیاء mock در واحد تحت تست خود جایگزین کنید. این کار اغلب با استفاده از تزریق وابستگی (dependency injection) انجام میشود.
- اجرای تست: تست خود را اجرا کنید و نحوه تعامل واحد تحت تست با توابع mock را مشاهده کنید.
- تأیید تعاملات: تأیید کنید که توابع mock با آرگومانها، مقادیر بازگشتی و تعداد دفعات مورد انتظار فراخوانی شدهاند.
- بازیابی عملکرد اصلی: پس از تست، با حذف اشیاء 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ها را ساده نگه دارید: توابع mock باید تا حد امکان ساده باشند تا از ایجاد باگ در تستهای شما جلوگیری شود.
- از تزریق وابستگی استفاده کنید: از تزریق وابستگی برای آسانتر کردن جایگزینی وابستگیهای واقعی با اشیاء mock استفاده کنید. تزریق از طریق سازنده (Constructor injection) ترجیح داده میشود زیرا وابستگیها را صریح میکند.
- تعاملات را تأیید کنید: همیشه تأیید کنید که واحد تحت تست شما به روش مورد انتظار با توابع mock تعامل دارد.
- عملکرد اصلی را بازیابی کنید: پس از هر تست، با حذف اشیاء mock و بازگشت به وابستگیهای واقعی، عملکرد اصلی را بازیابی کنید.
- Mockها را مستند کنید: توابع mock خود را به وضوح مستند کنید تا هدف و رفتار آنها را توضیح دهید.
- از مشخص کردن بیش از حد جزئیات خودداری کنید: بر روی هر تعامل جزئی تأکید نکنید، بلکه بر تعاملات کلیدی که برای رفتار مورد تست ضروری هستند، تمرکز کنید.
- تستهای یکپارچهسازی را در نظر بگیرید: در حالی که تستهای واحد با mockها مهم هستند، به یاد داشته باشید که آنها را با تستهای یکپارچهسازی (integration tests) که تعاملات بین کامپوننتهای واقعی را تأیید میکنند، تکمیل کنید.
جایگزینهای توابع Mock
در حالی که توابع mock ابزار قدرتمندی هستند، همیشه بهترین راهحل نیستند. در برخی موارد، تکنیکهای دیگر ممکن است مناسبتر باشند:
- Stubها (Stubs): Stubها سادهتر از mockها هستند. آنها پاسخهای از پیش تعریفشدهای به فراخوانیهای توابع ارائه میدهند، اما معمولاً نحوه انجام آن فراخوانیها را تأیید نمیکنند. آنها زمانی مفید هستند که فقط نیاز به کنترل ورودی به واحد تحت تست خود دارید.
- جاسوسها (Spies): Spies به شما امکان میدهند رفتار یک تابع واقعی را مشاهده کنید در حالی که هنوز به آن اجازه میدهند منطق اصلی خود را اجرا کند. آنها زمانی مفید هستند که میخواهید تأیید کنید که یک تابع با آرگومانهای خاص یا تعداد دفعات مشخصی فراخوانی شده است، بدون اینکه عملکرد آن را به طور کامل جایگزین کنید.
- جایگزینها (Fakes): Fakes پیادهسازیهای کارآمدی از یک وابستگی هستند، اما برای اهداف تست سادهسازی شدهاند. یک پایگاه داده در حافظه (in-memory database) نمونهای از یک fake است.
- تستهای یکپارچهسازی (Integration Tests): تستهای یکپارچهسازی تعاملات بین چندین کامپوننت را تأیید میکنند. آنها میتوانند جایگزین خوبی برای تستهای واحد با mockها باشند زمانی که میخواهید رفتار یک سیستم را به صورت کلی تست کنید.
نتیجهگیری
توابع mock ابزاری ضروری برای نوشتن تستهای واحد مؤثر هستند که به شما امکان ایزوله کردن واحدها، کنترل رفتار، شبیهسازی شرایط خطا و تست کدهای ناهمگام را میدهند. با پیروی از بهترین شیوهها و درک جایگزینها، میتوانید از توابع mock برای ساختن نرمافزاری قویتر، قابل اعتمادتر و قابل نگهداریتر بهره ببرید. به یاد داشته باشید که معاوضهها را در نظر بگیرید و تکنیک تست مناسب را برای هر موقعیت انتخاب کنید تا یک استراتژی تست جامع و مؤثر ایجاد کنید، صرف نظر از اینکه در کجای جهان در حال ساخت آن هستید.