تعد كتابة الاختبارات الجيدة إحدى مهارات المطورين الكبار. إلى جانب إعداد pytest الأساسي، يتطلب اختبار Python للإنتاج في عام 2026 فهم بنية الاختبار والتركيبات واستراتيجيات السخرية والاختبار القائم على الخاصية وتكامل CI. يغطي هذا الدليل أنماط الاختبار المتقدمة المستخدمة في مشاريع بايثون الاحترافية.
📋 Table of Contents
هندسة الاختبار: هرم الاختبار
Testing Pyramid:
/ E2E \ - Few, slow, expensive
/ Integration \ - Some, moderate
/ Unit Tests \ - Many, fast, cheap
Rule: 70% unit, 20% integration, 10% E2E
Good test properties:
- Fast: unit tests < 1ms each
- Independent: no shared state between tests
- Repeatable: same result every run
- Self-validating: clear pass/fail
- Timely: written alongside/before code
تركيبات متقدمة
import pytest
import asyncio
from unittest.mock import MagicMock, AsyncMock, patch
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
# Fixture scope hierarchy
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="session")
async def db_engine():
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
await create_all_tables(engine)
yield engine
await engine.dispose()
@pytest.fixture
async def db(db_engine):
async with AsyncSession(db_engine) as session:
yield session
await session.rollback() # clean state after each test
@pytest.fixture
def mock_email_service():
with patch("app.services.email.EmailService.send") as mock:
mock.return_value = True
yield mock
# Factory fixture pattern
@pytest.fixture
def make_user(db):
async def _factory(email="test@example.com", role="user", **kwargs):
user = User(email=email, role=role, **kwargs)
db.add(user)
await db.commit()
return user
return _factory
# Usage
async def test_user_creation(db, make_user, mock_email_service):
user = await make_user(email="alice@example.com")
assert user.id is not None
mock_email_service.assert_called_once_with(
to="alice@example.com",
subject="Welcome!"
)
الاختبار المبني على الملكية مع الفرضية
pip install hypothesis
from hypothesis import given, settings, strategies as st
from hypothesis.extra.pandas import data_frames, column
# Test with arbitrary inputs
@given(st.integers(), st.integers())
def test_add_commutative(a: int, b: int):
assert add(a, b) == add(b, a)
@given(st.lists(st.integers(), min_size=1))
def test_sort_preserves_length(lst: list[int]):
sorted_lst = sorted(lst)
assert len(sorted_lst) == len(lst)
@given(st.text(min_size=1, max_size=100))
def test_hash_always_returns_string(text: str):
result = hash_password(text)
assert isinstance(result, str)
assert len(result) > 0
# Complex data generation
@given(
st.builds(
CreateUserRequest,
email=st.emails(),
name=st.text(min_size=2, max_size=50).filter(str.strip),
password=st.text(min_size=8, max_size=128),
)
)
@settings(max_examples=50)
def test_user_creation_accepts_valid_data(request: CreateUserRequest):
user = create_user(request)
assert user.email == request.email.lower()
# Stateful testing
from hypothesis.stateful import RuleBasedStateMachine, rule, invariant
class QueueMachine(RuleBasedStateMachine):
def __init__(self):
super().__init__()
self.model = []
self.implementation = Queue()
@rule(value=st.integers())
def enqueue(self, value: int):
self.model.append(value)
self.implementation.enqueue(value)
@rule()
def dequeue(self):
if self.model:
expected = self.model.pop(0)
actual = self.implementation.dequeue()
assert actual == expected
@invariant()
def sizes_match(self):
assert len(self.model) == self.implementation.size()
السخرية من أفضل الممارسات
from unittest.mock import MagicMock, AsyncMock, patch, call
import pytest
# Mock at the right level — where it's USED, not where it's defined
# If service.py imports: from utils import send_email
# Mock: "service.send_email" not "utils.send_email"
def test_order_sends_confirmation(mocker):
# Mock async method
mock_send = mocker.patch("app.services.order.send_email", new=AsyncMock())
mock_send.return_value = True
order = asyncio.run(create_order(user_id=1, items=[...]))
mock_send.assert_called_once()
call_kwargs = mock_send.call_args.kwargs
assert call_kwargs["to"] == "alice@example.com"
assert "order confirmation" in call_kwargs["subject"].lower()
# Mock with side effects
def test_retry_on_network_error(mocker):
mock_get = mocker.patch("requests.get")
mock_get.side_effect = [
ConnectionError("Network error"), # first call fails
ConnectionError("Network error"), # second call fails
MagicMock(json=lambda: {"data": "success"}), # third succeeds
]
result = fetch_with_retry("https://api.example.com")
assert result == {"data": "success"}
assert mock_get.call_count == 3
# Context manager mock
def test_file_processing(mocker):
mock_open = mocker.mock_open(read_data="line1
line2
line3")
mocker.patch("builtins.open", mock_open)
result = count_lines("any_file.txt")
assert result == 3
اختبارات التكامل مع الخدمات الحقيقية
# Using pytest-docker for real database in CI
import pytest
import psycopg2
from testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="session")
def postgres():
with PostgresContainer("postgres:16") as pg:
yield pg
@pytest.fixture(scope="session")
def db_url(postgres):
return postgres.get_connection_url()
# Using respx to mock HTTP at socket level
import respx
import httpx
@pytest.mark.asyncio
async def test_external_api_integration():
with respx.mock:
respx.get("https://api.github.com/users/alice").mock(
return_value=httpx.Response(200, json={"login": "alice", "followers": 1000})
)
result = await fetch_github_user("alice")
assert result["followers"] == 1000
تغطية الاختبار ومقاييس الجودة
# pytest.ini or pyproject.toml
# [tool.pytest.ini_options]
# addopts = "--cov=app --cov-report=html --cov-report=term-missing --cov-fail-under=80"
# Run with coverage
pytest --cov=app --cov-report=html
# Mutation testing (verify your tests actually catch bugs)
pip install mutmut
mutmut run --paths-to-mutate app/services/
mutmut results # shows surviving mutants (untested code paths)
# Performance benchmarks
pip install pytest-benchmark
@pytest.mark.benchmark
def test_encryption_performance(benchmark):
result = benchmark(encrypt_password, "test_password")
assert len(result) > 0 # benchmark.stats shows timing
أفضل ممارسات اختبار Python في عام 2026: كتابة الاختبارات جنبًا إلى جنب مع التعليمات البرمجية، واستخدام تركيبات المصنع لبيانات الاختبار، والمحاكاة على الحدود وليس داخل التنفيذ، واستخدام الاختبار القائم على الخاصية للتحقق من صحة البيانات، وقياس درجة الطفرة للتحقق من أن الاختبارات تلتقط الأخطاء بالفعل. تغطية 80% هي الأرضية، وليس السقف – يجب أن تكون المسارات عالية القيمة 100%.
🔗 Share this article
✍️ Leave a Comment