أطلق العنان للإمكانات الكاملة لـ Pytest مع تقنيات التركيبات المتقدمة. تعلم كيفية الاستفادة من الاختبارات المعلميّة وتكامل المحاكاة لاختبارات Python قوية وفعالة.
إتقان تركيبات Pytest المتقدمة: الاختبارات المعلميّة وتكامل المحاكاة
Pytest هو إطار عمل اختبار قوي ومرن للغة Python. بساطته وقابليته للتوسيع تجعله المفضل لدى المطورين في جميع أنحاء العالم. واحدة من أكثر ميزات Pytest إقناعًا هي نظام التركيبات (fixtures)، الذي يسمح بإعدادات اختبار أنيقة وقابلة لإعادة الاستخدام. يتعمق هذا المقال في تقنيات التركيبات المتقدمة، مع التركيز بشكل خاص على الاختبارات المعلميّة (parameterized testing) وتكامل المحاكاة (mock integration). سنستكشف كيف يمكن لهذه التقنيات أن تعزز بشكل كبير سير عمل الاختبار لديك، مما يؤدي إلى كود أكثر قوة وقابلية للصيانة.
فهم تركيبات Pytest
قبل الغوص في الموضوعات المتقدمة، دعونا نلخص بإيجاز أساسيات تركيبات Pytest. التركيبة هي دالة تعمل قبل كل دالة اختبار يتم تطبيقها عليها. يتم استخدامها لتوفير خط أساس ثابت للاختبارات، مما يضمن الاتساق ويقلل من الكود المتكرر. يمكن للتركيبات أداء مهام مثل:
- إعداد اتصال بقاعدة البيانات
- إنشاء ملفات أو مجلدات مؤقتة
- تهيئة الكائنات بتكوينات محددة
- المصادقة مع واجهة برمجة تطبيقات (API)
تعزز التركيبات إعادة استخدام الكود وتجعل اختباراتك أكثر قابلية للقراءة والصيانة. يمكن تعريفها في نطاقات مختلفة (دالة، وحدة، جلسة) للتحكم في دورة حياتها واستهلاك الموارد.
مثال أساسي على التركيبة
إليك مثال بسيط لتركيبة Pytest تنشئ مجلدًا مؤقتًا:
import pytest
import tempfile
import os
@pytest.fixture
def temp_dir():
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
لاستخدام هذه التركيبة في اختبار، ما عليك سوى تضمينها كوسيط لدالة الاختبار الخاصة بك:
def test_create_file(temp_dir):
filepath = os.path.join(temp_dir, "test_file.txt")
with open(filepath, "w") as f:
f.write("Hello, world!")
assert os.path.exists(filepath)
الاختبارات المعلميّة مع Pytest
تسمح لك الاختبارات المعلميّة بتشغيل نفس دالة الاختبار عدة مرات بمجموعات مختلفة من بيانات الإدخال. وهذا مفيد بشكل خاص لاختبار الدوال ذات المدخلات والمخرجات المتوقعة المتغيرة. يوفر Pytest المزخرف @pytest.mark.parametrize لتنفيذ الاختبارات المعلميّة.
فوائد الاختبارات المعلميّة
- تقليل تكرار الكود: تجنب كتابة دوال اختبار متعددة شبه متطابقة.
- تحسين تغطية الاختبار: اختبار نطاق أوسع من قيم الإدخال بسهولة.
- تعزيز قابلية قراءة الاختبار: تحديد قيم الإدخال والمخرجات المتوقعة بوضوح لكل حالة اختبار.
مثال أساسي على التمرير المعلمي
لنفترض أن لديك دالة تجمع رقمين:
def add(x, y):
return x + y
يمكنك استخدام الاختبارات المعلميّة لاختبار هذه الدالة بقيم إدخال مختلفة:
import pytest
@pytest.mark.parametrize("x, y, expected", [
(1, 2, 3),
(5, 5, 10),
(-1, 1, 0),
(0, 0, 0),
])
def test_add(x, y, expected):
assert add(x, y) == expected
في هذا المثال، يحدد المزخرف @pytest.mark.parametrize أربع حالات اختبار، لكل منها قيم مختلفة لـ x و y والنتيجة المتوقعة. سيقوم Pytest بتشغيل دالة test_add أربع مرات، مرة لكل مجموعة من المعلمات.
تقنيات التمرير المعلمي المتقدمة
يقدم Pytest العديد من التقنيات المتقدمة للتمرير المعلمي، بما في ذلك:
- استخدام التركيبات مع التمرير المعلمي: دمج التركيبات مع التمرير المعلمي لتوفير إعدادات مختلفة لكل حالة اختبار.
- معرفات (IDs) لحالات الاختبار: تعيين معرفات مخصصة لحالات الاختبار لتحسين التقارير وتصحيح الأخطاء.
- التمرير المعلمي غير المباشر: تمرير الوسائط إلى التركيبات، مما يسمح بإنشاء تركيبات ديناميكية.
استخدام التركيبات مع التمرير المعلمي
يسمح لك هذا بتكوين التركيبات ديناميكيًا بناءً على المعلمات التي تم تمريرها إلى الاختبار. تخيل أنك تختبر دالة تتفاعل مع قاعدة بيانات. قد ترغب في استخدام تكوينات مختلفة لقاعدة البيانات (على سبيل المثال، سلاسل اتصال مختلفة) لحالات اختبار مختلفة.
import pytest
@pytest.fixture
def db_config(request):
if request.param == "prod":
return {"host": "prod.example.com", "port": 5432}
elif request.param == "test":
return {"host": "test.example.com", "port": 5433}
else:
raise ValueError("Invalid database environment")
@pytest.fixture
def db_connection(db_config):
# Simulate establishing a database connection
print(f"Connecting to database at {db_config['host']}:{db_config['port']}")
return f"Connection to {db_config['host']}"
@pytest.mark.parametrize("db_config", ["prod", "test"], indirect=True)
def test_database_interaction(db_connection):
# Your test logic here, using the db_connection fixture
print(f"Using connection: {db_connection}")
assert "Connection" in db_connection
في هذا المثال، تم تمرير معلمات إلى تركيبة db_config. الوسيط indirect=True يخبر Pytest بتمرير المعلمات ("prod" و "test") إلى دالة تركيبة db_config. ثم تُرجع تركيبة db_config تكوينات مختلفة لقاعدة البيانات بناءً على قيمة المعلمة. تستخدم تركيبة db_connection تركيبة db_config لإنشاء اتصال بقاعدة البيانات. وأخيرًا، تستخدم دالة test_database_interaction تركيبة db_connection للتفاعل مع قاعدة البيانات.
معرفات (IDs) لحالات الاختبار
توفر المعرفات المخصصة أسماء وصفية أكثر لحالات الاختبار الخاصة بك في تقرير الاختبار، مما يسهل تحديد الأخطاء وتصحيحها.
import pytest
@pytest.mark.parametrize(
"input_string, expected_output",
[
("hello", "HELLO"),
("world", "WORLD"),
("", ""),
],
ids=["lowercase_hello", "lowercase_world", "empty_string"],
)
def test_uppercase(input_string, expected_output):
assert input_string.upper() == expected_output
بدون معرفات، سينشئ Pytest أسماء عامة مثل test_uppercase[0]، test_uppercase[1]، إلخ. مع المعرفات، سيعرض تقرير الاختبار أسماء ذات معنى أكبر مثل test_uppercase[lowercase_hello].
التمرير المعلمي غير المباشر
يسمح لك التمرير المعلمي غير المباشر بتمرير معلمات إلى مدخلات التركيبة، بدلاً من دالة الاختبار مباشرة. يكون هذا مفيدًا عندما تريد إنشاء مثيلات تركيبات مختلفة بناءً على قيمة المعلمة.
import pytest
@pytest.fixture
def input_data(request):
if request.param == "valid":
return {"name": "John Doe", "email": "john.doe@example.com"}
elif request.param == "invalid":
return {"name": "", "email": "invalid-email"}
else:
raise ValueError("Invalid input data type")
def validate_data(data):
if not data["name"]:
return False, "Name cannot be empty"
if "@" not in data["email"]:
return False, "Invalid email address"
return True, "Valid data"
@pytest.mark.parametrize("input_data", ["valid", "invalid"], indirect=True)
def test_validate_data(input_data):
is_valid, message = validate_data(input_data)
if input_data == {"name": "John Doe", "email": "john.doe@example.com"}:
assert is_valid is True
assert message == "Valid data"
else:
assert is_valid is False
assert message in ["Name cannot be empty", "Invalid email address"]
في هذا المثال، تم تمرير معلمات إلى تركيبة input_data بالقيمتين "valid" و "invalid". الوسيط indirect=True يخبر Pytest بتمرير هذه القيم إلى دالة تركيبة input_data. ثم تُرجع تركيبة input_data قواميس بيانات مختلفة بناءً على قيمة المعلمة. بعد ذلك، تستخدم دالة test_validate_data تركيبة input_data لاختبار دالة validate_data ببيانات إدخال مختلفة.
المحاكاة (Mocking) مع Pytest
المحاكاة هي تقنية تستخدم لاستبدال التبعيات الحقيقية ببدائل خاضعة للتحكم (mocks) أثناء الاختبار. هذا يسمح لك بعزل الكود الذي يتم اختباره وتجنب الاعتماد على الأنظمة الخارجية، مثل قواعد البيانات أو واجهات برمجة التطبيقات (APIs) أو أنظمة الملفات.
فوائد المحاكاة
- عزل الكود: اختبار الكود بمعزل عن غيره، دون الاعتماد على التبعيات الخارجية.
- التحكم في السلوك: تحديد سلوك التبعيات، مثل القيم المرتجعة والاستثناءات.
- تسريع الاختبارات: تجنب الأنظمة الخارجية البطيئة أو غير الموثوقة.
- اختبار الحالات الهامشية: محاكاة ظروف الخطأ والحالات الهامشية التي يصعب إعادة إنتاجها في بيئة حقيقية.
استخدام مكتبة unittest.mock
توفر Python مكتبة unittest.mock لإنشاء كائنات المحاكاة (mocks). يتكامل Pytest بسلاسة مع unittest.mock، مما يسهل محاكاة التبعيات في اختباراتك.
مثال أساسي على المحاكاة
لنفترض أن لديك دالة تسترجع البيانات من واجهة برمجة تطبيقات خارجية:
import requests
def get_data_from_api(url):
response = requests.get(url)
response.raise_for_status() # Raise an exception for bad status codes
return response.json()
لاختبار هذه الدالة دون إجراء طلب فعلي لواجهة برمجة التطبيقات، يمكنك محاكاة دالة requests.get:
import pytest
import requests
from unittest.mock import patch
@patch("requests.get")
def test_get_data_from_api(mock_get):
# Configure the mock to return a specific response
mock_get.return_value.json.return_value = {"data": "test data"}
mock_get.return_value.status_code = 200
# Call the function being tested
data = get_data_from_api("https://example.com/api")
# Assert that the mock was called with the correct URL
mock_get.assert_called_once_with("https://example.com/api")
# Assert that the function returned the expected data
assert data == {"data": "test data"}
في هذا المثال، يستبدل المزخرف @patch("requests.get") دالة requests.get بكائن محاكاة. الوسيط mock_get هو كائن المحاكاة. يمكننا بعد ذلك تكوين كائن المحاكاة لإرجاع استجابة محددة والتأكد من أنه تم استدعاؤه بعنوان URL الصحيح.
المحاكاة باستخدام التركيبات
يمكنك أيضًا استخدام التركيبات لإنشاء وإدارة كائنات المحاكاة. يمكن أن يكون هذا مفيدًا لمشاركة كائنات المحاكاة عبر اختبارات متعددة أو لإنشاء إعدادات محاكاة أكثر تعقيدًا.
import pytest
import requests
from unittest.mock import Mock
@pytest.fixture
def mock_api_get():
mock = Mock()
mock.return_value.json.return_value = {"data": "test data"}
mock.return_value.status_code = 200
return mock
@pytest.fixture
def patched_get(mock_api_get, monkeypatch):
monkeypatch.setattr(requests, "get", mock_api_get)
return mock_api_get
def test_get_data_from_api(patched_get):
# Call the function being tested
data = get_data_from_api("https://example.com/api")
# Assert that the mock was called with the correct URL
patched_get.assert_called_once_with("https://example.com/api")
# Assert that the function returned the expected data
assert data == {"data": "test data"}
هنا، تنشئ mock_api_get كائن محاكاة وتعيده. ثم تستخدم patched_get تركيبة monkeypatch من pytest، لاستبدال requests.get الحقيقية بكائن المحاكاة. وهذا يسمح للاختبارات الأخرى باستخدام نفس نقطة نهاية API المحاكاة.
تقنيات المحاكاة المتقدمة
يقدم Pytest و unittest.mock العديد من تقنيات المحاكاة المتقدمة، بما في ذلك:
- الآثار الجانبية (Side Effects): تحديد سلوك مخصص لكائنات المحاكاة بناءً على وسائط الإدخال.
- محاكاة الخصائص (Property Mocking): محاكاة خصائص الكائنات.
- مديرو السياق (Context Managers): استخدام كائنات المحاكاة داخل مديري السياق للاستبدالات المؤقتة.
الآثار الجانبية
تسمح لك الآثار الجانبية بتحديد سلوك مخصص لكائنات المحاكاة الخاصة بك بناءً على وسائط الإدخال التي تتلقاها. هذا مفيد لمحاكاة سيناريوهات مختلفة أو ظروف خطأ.
import pytest
from unittest.mock import Mock
def test_side_effect():
mock = Mock()
mock.side_effect = [1, 2, 3]
assert mock() == 1
assert mock() == 2
assert mock() == 3
with pytest.raises(StopIteration):
mock()
يعيد كائن المحاكاة هذا 1، 2، و 3 في استدعاءات متتالية، ثم يثير استثناء StopIteration عند استنفاد القائمة.
محاكاة الخصائص
تسمح لك محاكاة الخصائص بمحاكاة سلوك الخصائص على الكائنات. هذا مفيد لاختبار الكود الذي يعتمد على خصائص الكائن بدلاً من الأساليب (methods).
import pytest
from unittest.mock import patch
class MyClass:
@property
def my_property(self):
return "original value"
def test_property_mocking():
obj = MyClass()
with patch.object(obj, "my_property", new_callable=pytest.PropertyMock) as mock_property:
mock_property.return_value = "mocked value"
assert obj.my_property == "mocked value"
يحاكي هذا المثال خاصية my_property لكائن MyClass، مما يسمح لك بالتحكم في قيمتها المرتجعة أثناء الاختبار.
مديرو السياق
يسمح لك استخدام كائنات المحاكاة داخل مديري السياق باستبدال التبعيات مؤقتًا لمجموعة محددة من الكود. هذا مفيد لاختبار الكود الذي يتفاعل مع الأنظمة الخارجية أو الموارد التي يجب محاكاتها لفترة محدودة فقط.
import pytest
from unittest.mock import patch
def test_context_manager_mocking():
with patch("os.path.exists") as mock_exists:
mock_exists.return_value = True
assert os.path.exists("dummy_path") is True
# The mock is automatically reverted after the 'with' block
# Ensure the original function is restored, although we can't really assert
# the real `os.path.exists` function's behavior without a real path.
# The important thing is that the patch is gone after the context.
print("Mock has been removed")
الجمع بين التمرير المعلمي والمحاكاة
يمكن دمج هاتين التقنيتين القويتين لإنشاء اختبارات أكثر تطورًا وفعالية. يمكنك استخدام التمرير المعلمي لاختبار سيناريوهات مختلفة بتكوينات محاكاة مختلفة.
import pytest
import requests
from unittest.mock import patch
def get_user_data(user_id):
url = f"https://api.example.com/users/{user_id}"
response = requests.get(url)
response.raise_for_status()
return response.json()
@pytest.mark.parametrize(
"user_id, expected_data",
[
(1, {"id": 1, "name": "John Doe"}),
(2, {"id": 2, "name": "Jane Smith"}),
],
)
@patch("requests.get")
def test_get_user_data(mock_get, user_id, expected_data):
mock_get.return_value.json.return_value = expected_data
mock_get.return_value.status_code = 200
data = get_user_data(user_id)
assert data == expected_data
mock_get.assert_called_once_with(f"https://api.example.com/users/{user_id}")
في هذا المثال، تم تمرير معلمات إلى دالة test_get_user_data بقيم user_id و expected_data مختلفة. يقوم المزخرف @patch بمحاكاة دالة requests.get. سيقوم Pytest بتشغيل دالة الاختبار مرتين، مرة لكل مجموعة من المعلمات، مع تكوين كائن المحاكاة لإرجاع expected_data المقابلة.
أفضل الممارسات لاستخدام التركيبات المتقدمة
- اجعل التركيبات مركزة: يجب أن يكون لكل تركيبة غرض واضح ومحدد.
- استخدم النطاقات المناسبة: اختر نطاق التركيبة المناسب (دالة، وحدة، جلسة) لتحسين استخدام الموارد.
- وثّق التركيبات: وثّق بوضوح غرض واستخدام كل تركيبة.
- تجنب الإفراط في المحاكاة: حاكِ فقط التبعيات الضرورية لعزل الكود الذي يتم اختباره.
- اكتب تأكيدات واضحة: تأكد من أن تأكيداتك واضحة ومحددة، وتتحقق من السلوك المتوقع للكود الذي يتم اختباره.
- فكر في التطوير الموجه بالاختبارات (TDD): اكتب اختباراتك قبل كتابة الكود، باستخدام التركيبات وكائنات المحاكاة لتوجيه عملية التطوير.
الخاتمة
توفر تقنيات التركيبات المتقدمة في Pytest، بما في ذلك الاختبارات المعلميّة وتكامل المحاكاة، أدوات قوية لكتابة اختبارات قوية وفعالة وقابلة للصيانة. من خلال إتقان هذه التقنيات، يمكنك تحسين جودة كود Python الخاص بك بشكل كبير وتبسيط سير عمل الاختبار. تذكر أن تركز على إنشاء تركيبات واضحة ومركزة، باستخدام النطاقات المناسبة، وكتابة تأكيدات شاملة. مع الممارسة، ستتمكن من الاستفادة من الإمكانات الكاملة لنظام تركيبات Pytest لإنشاء استراتيجية اختبار شاملة وفعالة.