أتقن مكتبة unittest.mock في Python. دليل شامل لكائنات الاختبار المزدوجة، الـ mocks، الـ stubs، الـ fakes، ومُزخرف الـ patch لاختبارات وحدة قوية ومعزولة.
كائنات Python Mock: دليل شامل لتطبيق Test Double
في عالم تطوير البرمجيات الحديث، لا تمثل كتابة الكود سوى نصف المعركة. يمثل ضمان أن الكود موثوق به وقوي ويعمل كما هو متوقع النصف الآخر، وهو أمر بالغ الأهمية بنفس القدر. وهنا يأتي دور الاختبار الآلي. يعد اختبار الوحدة، على وجه الخصوص، ممارسة أساسية تتضمن اختبار المكونات الفردية أو "الوحدات" للتطبيق بمعزل عن غيرها. ومع ذلك، غالبًا ما يكون هذا العزل أسهل قولًا من فعله. فالتطبيقات الحقيقية هي شبكات معقدة من الكائنات والخدمات والأنظمة الخارجية المترابطة. كيف يمكنك اختبار وظيفة واحدة إذا كانت تعتمد على قاعدة بيانات، أو واجهة برمجة تطبيقات لجهة خارجية، أو جزء آخر معقد من نظامك؟
تكمن الإجابة في تقنية قوية: استخدام كائنات الاختبار المزدوجة (Test Doubles). وفي بيئة بايثون، الأداة الأساسية لإنشائها هي مكتبة unittest.mock المتعددة الاستخدامات والتي لا غنى عنها. سيأخذك هذا الدليل في غوص عميق في عالم الـ mocks وكائنات الاختبار المزدوجة في بايثون. سنستكشف "السبب" وراءها، ونزيل الغموض عن أنواعها المختلفة، ونقدم أمثلة عملية من العالم الحقيقي باستخدام unittest.mock لمساعدتك في كتابة اختبارات أنظف وأسرع وأكثر فعالية.
ما هي كائنات الاختبار المزدوجة (Test Doubles) ولماذا نحتاج إليها؟
تخيل أنك تقوم بإنشاء دالة تسترجع ملف تعريف المستخدم من قاعدة بيانات شركتك ثم تقوم بتنسيقه. قد يبدو توقيع الدالة هكذا: get_formatted_user_profile(user_id, db_connection).
لاختبار هذه الدالة، ستواجه عدة تحديات:
- الاعتماد على نظام حي: سيحتاج اختبارك إلى قاعدة بيانات قيد التشغيل. وهذا يجعل الاختبارات بطيئة، ومعقدة الإعداد، وتعتمد على حالة النظام الخارجي وتوفره.
- عدم القدرة على التنبؤ: قد تتغير البيانات في قاعدة البيانات، مما يتسبب في فشل اختبارك حتى لو كان منطق التنسيق الخاص بك صحيحًا. وهذا يجعل الاختبارات "متقلبة" أو غير حتمية.
- صعوبة في اختبار الحالات الهامشية: كيف ستختبر ما يحدث إذا فشل اتصال قاعدة البيانات، أو إذا أعاد مستخدمًا يفتقر إلى بعض البيانات؟ محاكاة هذه السيناريوهات المحددة باستخدام قاعدة بيانات حقيقية يمكن أن يكون صعبًا للغاية.
كائن الاختبار المزدوج (Test Double) هو مصطلح عام لأي كائن يحل محل كائن حقيقي أثناء الاختبار. باستبدال db_connection الحقيقي بكائن اختبار مزدوج، يمكننا قطع الاعتماد على قاعدة البيانات الفعلية والتحكم الكامل في بيئة الاختبار.
يوفر استخدام كائنات الاختبار المزدوجة (Test Doubles) عدة فوائد رئيسية:
- العزل: تسمح لك باختبار وحدة التعليمات البرمجية الخاصة بك (مثل منطق التنسيق) بمعزل تام عن تبعياتها (مثل قاعدة البيانات). إذا فشل الاختبار، فأنت تعلم أن المشكلة في الوحدة قيد الاختبار، وليس في مكان آخر.
- السرعة: استبدال العمليات البطيئة مثل طلبات الشبكة أو استعلامات قاعدة البيانات بكائن اختبار مزدوج في الذاكرة يجعل مجموعة اختباراتك تعمل بشكل أسرع بكثير. يتم تشغيل الاختبارات السريعة في كثير من الأحيان، مما يؤدي إلى حلقة ملاحظات أضيق للمطورين.
- الحتمية: يمكنك تكوين كائن الاختبار المزدوج لإرجاع بيانات يمكن التنبؤ بها في كل مرة يتم فيها تشغيل الاختبار. وهذا يقضي على الاختبارات المتقلبة ويضمن أن الاختبار الفاشل يشير إلى مشكلة حقيقية.
- القدرة على اختبار الحالات الهامشية: يمكنك بسهولة تكوين كائن مزدوج لمحاكاة ظروف الخطأ، مثل إثارة
ConnectionErrorأو إرجاع بيانات فارغة، مما يسمح لك بالتحقق من أن التعليمات البرمجية الخاصة بك تتعامل مع هذه المواقف بأناقة.
تصنيف كائنات الاختبار المزدوجة (Test Doubles): ما وراء مجرد "Mocks"
بينما يستخدم المطورون غالبًا مصطلح "mock" بشكل عام للإشارة إلى أي كائن اختبار مزدوج، من المفيد فهم المصطلحات الأكثر دقة التي صاغها جيرارد ميسزاروس في كتابه "xUnit Test Patterns". معرفة هذه الفروقات تساعدك على التفكير بوضوح أكبر فيما تحاول تحقيقه في اختبارك.
1. Dummy (كائن صوري)
الكائن الصوري (Dummy) هو أبسط كائن اختبار مزدوج. يتم تمريره لملء قائمة المعلمات ولكنه لا يستخدم أبدًا بالفعل. لا يتم استدعاء أساليبه عادةً. يمكنك استخدام كائن صوري عندما تحتاج إلى توفير وسيطة لدالة، ولكنك لا تهتم بسلوك تلك الوسيطة في سياق الاختبار المحدد.
مثال: إذا كانت دالة تتطلب كائن 'logger' ولكن اختبارك لا يهتم بما يتم تسجيله، فيمكنك تمرير كائن صوري.
2. Fake (كائن زائف)
الكائن الزائف (Fake) له تطبيق عملي، ولكنه نسخة أبسط بكثير من كائن الإنتاج. لا يستخدم موارد خارجية ويستبدل تنفيذًا خفيف الوزن بآخر ثقيل. المثال الكلاسيكي هو قاعدة بيانات في الذاكرة تحل محل اتصال قاعدة بيانات حقيقي. إنه يعمل بالفعل—يمكنك إضافة بيانات إليه وقراءتها منه— ولكنه مجرد قاموس أو قائمة بسيطة تحت الغطاء.
3. Stub (كائن محدد الإجابات)
يوفر الـ Stub (كائن محدد الإجابات) إجابات "جاهزة" ومبرمجة مسبقًا لاستدعاءات الدوال التي تتم أثناء الاختبار. يُستخدم عندما تحتاج أن يتلقى الكود الخاص بك بيانات محددة من تبعية ما. على سبيل المثال، يمكنك تحديد إجابة لدالة مثل api_client.get_user(user_id=123) لإرجاع قاموس مستخدم محدد دائمًا، دون إجراء استدعاء API فعليًا.
4. Spy (كائن تجسسي)
الـ Spy (كائن تجسسي) هو كائن Stub يسجل أيضًا بعض المعلومات حول كيفية استدعائه. على سبيل المثال، قد يسجل عدد المرات التي تم فيها استدعاء دالة أو الوسائط التي تم تمريرها إليها. يتيح لك هذا "التجسس" على التفاعل بين الكود الخاص بك واعتماده ثم إجراء تأكيدات حول هذا التفاعل بعد وقوعه.
5. Mock (كائن وهمي)
الـ Mock (كائن وهمي) هو النوع الأكثر "وعيًا" من كائنات الاختبار المزدوجة. إنه كائن مبرمج مسبقًا بتوقعات بشأن الدوال التي سيتم استدعاؤها، وبأي وسائط، وبأي ترتيب. عادةً ما يفشل الاختبار الذي يستخدم كائن وهمي ليس فقط إذا أنتج الكود قيد الاختبار نتيجة خاطئة، ولكن أيضًا إذا لم يتفاعل مع الكائن الوهمي بالطريقة المتوقعة تمامًا. تعتبر الـ Mocks رائعة للتحقق من السلوك — ضمان حدوث تسلسل معين من الإجراءات.
توفر مكتبة unittest.mock في بايثون فئة واحدة قوية يمكن أن تعمل كـ Stub أو Spy أو Mock، اعتمادًا على كيفية استخدامك لها.
تقديم قوة بايثون: مكتبة \`unittest.mock\`
تُعد unittest.mock، وهي جزء من مكتبة بايثون القياسية منذ الإصدار 3.3، الحل الأساسي لإنشاء كائنات الاختبار المزدوجة. إن مرونتها وقوتها تجعلانها أداة أساسية لأي مطور بايثون جاد. إذا كنت تستخدم إصدارًا أقدم من بايثون، يمكنك تثبيت المكتبة المدعومة مسبقًا عبر pip: pip install mock.
يتمركز جوهر المكتبة حول فئتين رئيسيتين: Mock وشقيقها الأكثر قدرة، MagicMock. تم تصميم هذه الكائنات لتكون مرنة بشكل لا يصدق، حيث تنشئ سمات ودوال أثناء التنقل عند الوصول إليها.
غوص عميق: فئتا \`Mock\` و \`MagicMock\`
كائن \`Mock\`
كائن \`Mock\` هو كائن متغير. يمكنك إنشاء واحد، وسوف يستجيب فورًا لأي وصول إلى سمة أو استدعاء دالة، ويعيد كائن Mock آخر افتراضيًا. يتيح لك هذا ربط الاستدعاءات معًا بسهولة أثناء الإعداد.
# In a test file...
from unittest.mock import Mock
# Create a mock object
mock_api = Mock()
# Accessing an attribute creates it and returns another mock
print(mock_api.users)
# Output: <Mock name='mock.users' id='...'>
# Calling a method also returns a mock by default
print(mock_api.users.get(id=1))
# Output: <Mock name='mock.users.get()' id='...'>
هذا السلوك الافتراضي ليس مفيدًا جدًا للاختبار. تكمن القوة الحقيقية في تهيئة الـ mock ليتصرف مثل الكائن الذي يحل محله.
تهيئة قيم الإرجاع والآثار الجانبية
يمكنك إخبار دالة mock بما يجب إرجاعه باستخدام سمة return_value. هذه هي الطريقة التي تنشئ بها Stub (كائن محدد الإجابات).
from unittest.mock import Mock
# Create a mock for a data service
mock_service = Mock()
# Configure the return value for a method call
mock_service.get_data.return_value = {'id': 1, 'name': 'Test Data'}
# Now when we call it, we get our configured value
result = mock_service.get_data()
print(result)
# Output: {'id': 1, 'name': 'Test Data'}
لمحاكاة الأخطاء، يمكنك استخدام سمة side_effect. هذا مثالي لاختبار كيفية معالجة الكود الخاص بك للأخطاء.
from unittest.mock import Mock
mock_service = Mock()
# Configure the method to raise an exception
mock_service.get_data.side_effect = ConnectionError("Failed to connect to service")
# Calling the method will now raise the exception
try:
mock_service.get_data()
except ConnectionError as e:
print(e)
# Output: Failed to connect to service
دوال التأكيد للتحقق
تعمل كائنات الـ Mock أيضًا كـ Spies (كائنات تجسسية) و Mocks (كائنات وهمية) عن طريق تسجيل كيفية استخدامها. يمكنك بعد ذلك استخدام مجموعة من دوال التأكيد المضمنة للتحقق من هذه التفاعلات.
mock_object.method.assert_called(): تؤكد أن الدالة تم استدعاؤها مرة واحدة على الأقل.mock_object.method.assert_called_once(): تؤكد أن الدالة تم استدعاؤها مرة واحدة بالضبط.mock_object.method.assert_called_with(*args, **kwargs): تؤكد أن الدالة تم استدعاؤها آخر مرة بالوسائط المحددة.mock_object.method.assert_any_call(*args, **kwargs): تؤكد أن الدالة تم استدعاؤها بهذه الوسائط في أي نقطة.mock_object.method.assert_not_called(): تؤكد أن الدالة لم يتم استدعاؤها أبدًا.mock_object.call_count: خاصية عدد صحيح تخبرك بعدد مرات استدعاء الدالة.
from unittest.mock import Mock
mock_notifier = Mock()
# Imagine this is our function under test
def process_and_notify(data, notifier):
if data.get('critical'):
notifier.send_alert(message="Critical event occurred!")
# Test case 1: Critical data
process_and_notify({'critical': True}, mock_notifier)
mock_notifier.send_alert.assert_called_once_with(message="Critical event occurred!")
# Reset the mock for the next test
mock_notifier.reset_mock()
# Test case 2: Non-critical data
process_and_notify({'critical': False}, mock_notifier)
mock_notifier.send_alert.assert_not_called()
كائن \`MagicMock\`
الـ \`MagicMock\` هي فئة فرعية من \`Mock\` مع اختلاف رئيسي: لديها تطبيقات افتراضية لمعظم دوال بايثون "السحرية" أو "المخفية" (مثل __len__، __str__، __iter__). إذا حاولت استخدام \`Mock\` عادي في سياق يتطلب إحدى هذه الدوال، فستحصل على خطأ.
from unittest.mock import Mock, MagicMock
# Using a regular Mock
mock_list = Mock()
try:
len(mock_list)
except TypeError as e:
print(e) # Output: 'Mock' object has no len()
# Using a MagicMock
magic_mock_list = MagicMock()
print(len(magic_mock_list)) # Output: 0 (by default)
# We can configure the magic method's return value too
magic_mock_list.__len__.return_value = 100
print(len(magic_mock_list)) # Output: 100
قاعدة عامة: ابدأ بـ \`MagicMock\`. إنها أكثر أمانًا بشكل عام وتغطي المزيد من حالات الاستخدام، مثل محاكاة الكائنات التي تُستخدم في حلقات for (التي تتطلب __iter__) أو عبارات with (التي تتطلب __enter__ و __exit__).
تطبيق عملي: المُزخرف \`patch\` ومدير السياق
إنشاء كائن وهمي شيء، ولكن كيف تجعل الكود الخاص بك يستخدمه بدلاً من الكائن الحقيقي؟ هنا يأتي دور \`patch\`. \`patch\` هي أداة قوية في \`unittest.mock\` تقوم باستبدال كائن مستهدف مؤقتًا بكائن وهمي طوال مدة الاختبار.
\`@patch\` كمُزخرف
الطريقة الأكثر شيوعًا لاستخدام \`patch\` هي كمُزخرف على دالة الاختبار الخاصة بك. تقوم بتوفير مسار السلسلة للكائن الذي تريد استبداله.
لنفترض أن لدينا دالة تستجلب البيانات من واجهة برمجة تطبيقات ويب باستخدام مكتبة \`requests\` الشائعة:
# in file: my_app/data_fetcher.py
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status() # Raise an exception for bad status codes
return response.json()
نريد اختبار هذه الدالة دون إجراء استدعاء شبكة حقيقي. يمكننا استخدام \`patch\` على \`requests.get\`:
# in file: tests/test_data_fetcher.py
import unittest
from unittest.mock import patch, Mock
from my_app.data_fetcher import get_user_data
class TestDataFetcher(unittest.TestCase):
@patch('my_app.data_fetcher.requests.get')
def test_get_user_data_success(self, mock_get):
"""Test successful data fetching."""
# Configure the mock to simulate a successful API response
mock_response = Mock()
mock_response.json.return_value = {'id': 1, 'name': 'John Doe'}
mock_response.raise_for_status.return_value = None # Do nothing on success
mock_get.return_value = mock_response
# Call our function
user_data = get_user_data(1)
# Assert our function made the correct API call
mock_get.assert_called_once_with('https://api.example.com/users/1')
# Assert our function returned the expected data
self.assertEqual(user_data, {'id': 1, 'name': 'John Doe'})
لاحظ كيف ينشئ \`patch\` كائن \`MagicMock\` ويمرره إلى دالة الاختبار الخاصة بنا كوسيطة \`mock_get\`. داخل الاختبار، أي استدعاء لـ \`requests.get\` داخل my_app.data_fetcher يتم إعادة توجيهه إلى كائن الـ mock الخاص بنا.
\`patch\` كمدير سياق
أحيانًا تحتاج فقط إلى تفعيل \`patch\` ل جزء صغير من الاختبار. استخدام \`patch\` كمدير سياق مع عبارة \`with\` مثالي لذلك.
# in file: tests/test_data_fetcher.py
import unittest
from unittest.mock import patch, Mock
from my_app.data_fetcher import get_user_data
class TestDataFetcher(unittest.TestCase):
def test_get_user_data_with_context_manager(self):
"""Test using patch as a context manager."""
with patch('my_app.data_fetcher.requests.get') as mock_get:
# Configure the mock inside the 'with' block
mock_response = Mock()
mock_response.json.return_value = {'id': 2, 'name': 'Jane Doe'}
mock_get.return_value = mock_response
user_data = get_user_data(2)
mock_get.assert_called_once_with('https://api.example.com/users/2')
self.assertEqual(user_data, {'id': 2, 'name': 'Jane Doe'})
# Outside the 'with' block, requests.get is back to its original state
مفهوم حاسم: أين يتم التفعيل بـ \`patch\`؟
هذا هو المصدر الأكثر شيوعًا للارتباك عند استخدام \`patch\`. القاعدة هي: يجب عليك تفعيل \`patch\` على الكائن حيث يتم البحث عنه، وليس حيث يتم تعريفه.
دعنا نوضح بمثال. لنفترض أن لدينا ملفين:
# in file: services.py
class Database:
def connect(self):
# ... complex connection logic ...
return "REAL_CONNECTION"
# in file: main_app.py
from services import Database
def start_app():
db = Database()
connection = db.connect()
print(f"Got connection: {connection}")
return connection
الآن، نريد اختبار \`start_app\` في \`main_app.py\` دون إنشاء كائن \`Database\` حقيقي. خطأ شائع هو محاولة استخدام \`patch\` على \`services.Database\`.
# in file: test_main_app.py
import unittest
from unittest.mock import patch
from main_app import start_app
class TestApp(unittest.TestCase):
# THIS IS THE WRONG WAY TO PATCH!
@patch('services.Database')
def test_start_app_incorrectly(self, mock_db):
start_app()
# This test will still use the REAL Database class!
# THIS IS THE CORRECT WAY TO PATCH!
@patch('main_app.Database')
def test_start_app_correctly(self, mock_db_class):
# We are patching 'Database' in the 'main_app' namespace
# Configure the mock instance that will be created
mock_instance = mock_db_class.return_value
mock_instance.connect.return_value = "MOCKED_CONNECTION"
connection = start_app()
# Assert that our mock was used
mock_db_class.assert_called_once() # Was the class instantiated?
mock_instance.connect.assert_called_once() # Was the connect method called?
self.assertEqual(connection, "MOCKED_CONNECTION")
لماذا يفشل الاختبار الأول؟ لأن \`main_app.py\` ينفذ from services import Database. هذا يستورد فئة \`Database\` إلى مساحة أسماء وحدة \`main_app\`. عندما تعمل \`start_app\`، فإنها تبحث عن \`Database\` داخل وحدتها الخاصة (\`main_app\`). تفعيل \`patch\` على \`services.Database\` يغيرها في وحدة \`services\`، ولكن \`main_app\` لديها بالفعل مرجعها الخاص إلى الفئة الأصلية. النهج الصحيح هو تفعيل \`patch\` على \`main_app.Database\`، وهو الاسم الذي يستخدمه الكود قيد الاختبار بالفعل.
تقنيات Mocking المتقدمة
\`spec\` و \`autospec\`: جعل الـ Mocks أكثر أمانًا
كائن \`MagicMock\` القياسي له جانب سلبي محتمل: سيسمح لك باستدعاء أي دالة بأي وسيطات، حتى لو لم تكن تلك الدالة موجودة في الكائن الحقيقي. هذا يمكن أن يؤدي إلى اختبارات تمر ولكنها تخفي مشاكل حقيقية، مثل الأخطاء الإملائية في أسماء الدوال أو التغييرات في واجهة برمجة تطبيقات كائن حقيقي.
# Real class
class Notifier:
def send_message(self, text):
# ... sends message ...
pass
# A test with a typo
from unittest.mock import MagicMock
mock_notifier = MagicMock()
# Oops, a typo! The real method is send_message
mock_notifier.send_mesage("hello") # No error is raised!
mock_notifier.send_mesage.assert_called_with("hello") # This assertion passes!
# Our test is green, but the production code would fail.
لمنع ذلك، توفر \`unittest.mock\` الوسيطات \`spec\` و \`autospec\`.
- \`spec=SomeClass\`: هذا يقوم بتهيئة الـ mock ليكون له نفس واجهة برمجة التطبيقات (API) مثل \`SomeClass\`. إذا حاولت الوصول إلى دالة أو سمة غير موجودة في الفئة الحقيقية، فسيتم إثارة خطأ \`AttributeError\`.
- \`autospec=True\` (أو \`autospec=SomeClass\`): هذا أكثر قوة. إنه يعمل مثل \`spec\`، ولكنه يتحقق أيضًا من توقيع استدعاء أي دوال وهمية. إذا قمت باستدعاء دالة بعدد خاطئ أو بأسماء وسيطات خاطئة، فسيتم إثارة خطأ \`TypeError\`، تمامًا كما سيفعل الكائن الحقيقي.
from unittest.mock import create_autospec
# Create a mock that has the same interface as our Notifier class
spec_notifier = create_autospec(Notifier)
try:
# This will fail immediately because of the typo
spec_notifier.send_mesage("hello")
except AttributeError as e:
print(e) # Output: Mock object has no attribute 'send_mesage'
try:
# This will fail because the signature is wrong (no 'text' keyword)
spec_notifier.send_message("hello")
except TypeError as e:
print(e) # Output: missing a required argument: 'text'
# This is the correct way to call it
spec_notifier.send_message(text="hello") # This works!
spec_notifier.send_message.assert_called_once_with(text="hello")
أفضل الممارسات: استخدم دائمًا \`autospec=True\` عند استخدام \`patch\`. يجعل هذا اختباراتك أكثر قوة وأقل هشاشة. \`@patch('path.to.thing', autospec=True)\`.
مثال واقعي: اختبار خدمة معالجة البيانات
دعنا نربط كل شيء معًا بمثال أكثر اكتمالًا. لدينا \`ReportGenerator\` يعتمد على قاعدة بيانات ونظام ملفات.
# in file: app/services.py
class DatabaseConnector:
def get_sales_data(self, start_date, end_date):
# In reality, this would query a database
raise NotImplementedError("This should not be called in tests")
class FileSaver:
def save_report(self, path, content):
# In reality, this would write to a file
raise NotImplementedError("This should not be called in tests")
# in file: app/reports.py
from .services import DatabaseConnector, FileSaver
class ReportGenerator:
def __init__(self):
self.db_connector = DatabaseConnector()
self.file_saver = FileSaver()
def generate_sales_report(self, start_date, end_date, output_path):
"""Fetches sales data and saves a formatted report."""
raw_data = self.db_connector.get_sales_data(start_date, end_date)
if not raw_data:
report_content = "No sales data for this period."
else:
total_sales = sum(item['amount'] for item in raw_data)
report_content = f"Total Sales from {start_date} to {end_date}: ${total_sales:.2f}"
self.file_saver.save_report(path=output_path, content=report_content)
return True
الآن، دعنا نكتب اختبار وحدة لدالة \`ReportGenerator.generate_sales_report\` التي تقوم بمحاكاة تبعياتها.
# in file: tests/test_reports.py
import unittest
from datetime import date
from unittest.mock import patch, Mock
from app.reports import ReportGenerator
class TestReportGenerator(unittest.TestCase):
@patch('app.reports.FileSaver', autospec=True)
@patch('app.reports.DatabaseConnector', autospec=True)
def test_generate_sales_report_with_data(self, mock_db_connector_class, mock_file_saver_class):
"""Test report generation when the database returns data."""
# Arrange: Setup our mocks
mock_db_instance = mock_db_connector_class.return_value
mock_file_saver_instance = mock_file_saver_class.return_value
# Configure the database mock to return some fake data (Stub)
fake_data = [
{'id': 1, 'amount': 100.50},
{'id': 2, 'amount': 75.00},
{'id': 3, 'amount': 25.25}
]
mock_db_instance.get_sales_data.return_value = fake_data
start = date(2023, 1, 1)
end = date(2023, 1, 31)
path = '/reports/sales_jan_2023.txt'
# Act: Create an instance of our class and call the method
generator = ReportGenerator()
result = generator.generate_sales_report(start, end, path)
# Assert: Verify the interactions and results
# 1. Was the database called correctly?
mock_db_instance.get_sales_data.assert_called_once_with(start, end)
# 2. Was the file saver called with the correct, calculated content?
expected_content = "Total Sales from 2023-01-01 to 2023-01-31: $200.75"
mock_file_saver_instance.save_report.assert_called_once_with(
path=path,
content=expected_content
)
# 3. Did our method return the correct value?
self.assertTrue(result)
يعزل هذا الاختبار بشكل مثالي المنطق داخل \`generate_sales_report\` عن تعقيدات قاعدة البيانات ونظام الملفات، مع التحقق في الوقت نفسه من أنه يتفاعل معها بشكل صحيح.
أفضل الممارسات للـ Mocking الفعال
- حافظ على بساطة الـ Mocks: الاختبار الذي يتطلب تكوين mock معقد جدًا غالبًا ما يكون علامة ( "رائحة اختبار") على أن الوحدة قيد الاختبار معقدة جدًا وقد تنتهك مبدأ المسؤولية الفردية. فكر في إعادة هيكلة كود الإنتاج.
- قم بمحاكاة المتعاونين، وليس كل شيء: يجب عليك فقط محاكاة الكائنات التي تتواصل معها الوحدة قيد الاختبار (المتعاونون معها). لا تقم بمحاكاة الكائن الذي تختبره نفسه.
- فضل \`autospec=True\`: كما ذكرنا، هذا يجعل اختباراتك أكثر قوة من خلال التأكد من أن واجهة الـ mock تتطابق مع واجهة الكائن الحقيقي. وهذا يساعد على اكتشاف المشكلات الناتجة عن إعادة الهيكلة.
- Mock واحد لكل اختبار (من الناحية المثالية): يركز اختبار الوحدة الجيد على سلوك أو تفاعل واحد. إذا وجدت نفسك تقوم بمحاكاة العديد من الكائنات المختلفة في اختبار واحد، فقد يكون من الأفضل تقسيمه إلى اختبارات متعددة وأكثر تركيزًا.
- كن محددًا في تأكيداتك: لا تكتفِ بالتحقق من \`mock.method.assert_called()\`. استخدم \`assert_called_with(...)\` للتأكد من أن التفاعل حدث بالبيانات الصحيحة. هذا يجعل اختباراتك أكثر قيمة.
- اختباراتك هي وثائق: استخدم أسماء واضحة و وصفية لاختباراتك وكائنات الـ mock (مثل \`mock_api_client\`، \`test_login_fails_on_network_error\`). هذا يجعل الغرض من الاختبار واضحًا للمطورين الآخرين.
الخاتمة
كائنات الاختبار المزدوجة ليست مجرد أداة للاختبار؛ إنها جزء أساسي من تصميم برمجيات قابلة للاختبار، نمطية، وقابلة للصيانة. باستبدال التبعيات الحقيقية ببدائل خاضعة للتحكم، يمكنك إنشاء مجموعة اختبار سريعة وموثوقة وقادرة على التحقق من كل زاوية من منطق تطبيقك.
توفر مكتبة unittest.mock في بايثون مجموعة أدوات عالمية المستوى لتطبيق هذه الأنماط. من خلال إتقان MagicMock، \`patch\`، وأمان \`autospec\`، تفتح القدرة على كتابة اختبارات وحدة معزولة حقًا. هذا يمكّنك من بناء تطبيقات معقدة بثقة، مع العلم أن لديك شبكة أمان من الاختبارات الدقيقة والمستهدفة لاكتشاف الانحدارات والتحقق من الميزات الجديدة. لذا انطلق، ابدأ في استخدام الـ patch، وقم ببناء تطبيقات بايثون أكثر قوة اليوم.