⏱️4 min read · 754 words
pytest هو المعيار الذهبي لاختبار Python في عام 2026. يستخدمه Django وFastAPI وNumPy والآلاف من المشاريع مفتوحة المصدر، pytest يجعل كتابة الاختبارات وتشغيلها أمرًا بديهيًا وقويًا. يأخذك هذا الدليل الكامل من اختبارك الأول إلى التركيبات المتقدمة، والمواصفات، وتكامل CI.
📋 Table of Contents
لماذا pytest على Unittest؟
- بناء الجملة أبسط– سهل
assertعبارات مع رسائل فشل مفيدة - تركيبات قوية– حقن التبعية مع التفكيك التلقائي
- بارامتريزي– إجراء اختبار واحد مع العديد من المدخلات
- النظام البيئي المساعد الغني— التغطية، وxdist، وmock، وasyncio، والمزيد
- اكتشاف أفضل— يجد الاختبارات تلقائيًا بدون نموذج معياري
التثبيت والاختبار الأول
# Install pytest
pip install pytest pytest-cov pytest-asyncio
# Create a test
# test_math.py
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
# Run tests
pytest # run all tests
pytest test_math.py # specific file
pytest -v # verbose output
pytest -k "add" # run tests matching name
pytest --tb=short # shorter tracebacks
منظمة الاختبار
# Recommended structure
# tests/
# conftest.py — shared fixtures
# test_users.py
# test_orders.py
# integration/
# test_api.py
# unit/
# test_services.py
# pytest.ini or pyproject.toml
# [tool.pytest.ini_options]
# testpaths = ["tests"]
# addopts = "-v --tb=short"
# Test class grouping
class TestUserService:
def test_create_user(self):
user = create_user("alice@example.com")
assert user.email == "alice@example.com"
def test_create_user_invalid_email(self):
with pytest.raises(ValueError, match="Invalid email"):
create_user("not-an-email")
def test_duplicate_email_raises(self, db):
create_user("alice@example.com")
with pytest.raises(DuplicateError):
create_user("alice@example.com")
المباريات – قوة pytest الخارقة
import pytest
from app.database import Database
from app.models import User
# Simple fixture
@pytest.fixture
def sample_user():
return User(id=1, name="Alice", email="alice@example.com")
# Fixture with setup and teardown
@pytest.fixture
def db():
database = Database(url="sqlite:///:memory:")
database.create_tables()
yield database # test runs here
database.drop_tables() # teardown
# Session-scoped fixture — runs once per test session
@pytest.fixture(scope="session")
def app_client():
from app import create_app
app = create_app(testing=True)
return app.test_client()
# Fixture using other fixtures
@pytest.fixture
def admin_user(db):
user = db.create_user(email="admin@test.com", role="admin")
return user
# conftest.py — shared fixtures auto-discovered
# pytest finds conftest.py automatically
Parametrize – اختبار حالات متعددة
import pytest
def is_valid_email(email: str) -> bool:
import re
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
@pytest.mark.parametrize("email,expected", [
("alice@example.com", True),
("user+tag@domain.co.uk", True),
("invalid", False),
("no-at-sign", False),
("@nodomain.com", False),
("user@.com", False),
("", False),
])
def test_email_validation(email, expected):
assert is_valid_email(email) == expected
# Multiple parametrize decorators — creates cartesian product
@pytest.mark.parametrize("base", [2, 10, 16])
@pytest.mark.parametrize("exp", [1, 2, 3])
def test_powers(base, exp):
result = base ** exp
assert result == pow(base, exp)
السخرية باستخدام pytest-mock
pip install pytest-mock
import pytest
from app.services import UserService
from app.email import EmailSender
def test_registration_sends_welcome_email(mocker):
# Mock the email sender
mock_send = mocker.patch("app.services.EmailSender.send")
service = UserService()
service.register("alice@example.com", "securepass")
# Assert email was sent
mock_send.assert_called_once_with(
to="alice@example.com",
subject="Welcome to TechPulse!"
)
def test_external_api_timeout(mocker):
# Mock requests.get to simulate timeout
mocker.patch(
"requests.get",
side_effect=requests.exceptions.Timeout
)
with pytest.raises(ServiceUnavailableError):
fetch_weather("London")
# Spy on real method (calls real code but tracks calls)
def test_cache_used_on_second_call(mocker):
spy = mocker.spy(cache_service, "get")
fetch_user(1)
fetch_user(1) # should hit cache
assert spy.call_count == 2
اختبار رمز غير المتزامن
pip install pytest-asyncio
import pytest
import pytest_asyncio
# Mark entire file as async
# pytest.ini: asyncio_mode = auto
@pytest.mark.asyncio
async def test_async_fetch():
async with AsyncClient(app=app, base_url="http://test") as client:
response = await client.get("/api/users")
assert response.status_code == 200
# Async fixture
@pytest_asyncio.fixture
async def async_db():
async with AsyncDatabase() as db:
await db.create_tables()
yield db
await db.drop_tables()
تقارير التغطية
# Run with coverage
pytest --cov=app --cov-report=html --cov-report=term-missing
# Output:
# Name Stmts Miss Cover
# app/models.py 45 3 93%
# app/services.py 87 12 86%
# TOTAL 132 15 89%
# Fail if coverage below threshold
pytest --cov=app --cov-fail-under=80
# .coveragerc or pyproject.toml
# [tool.coverage.run]
# omit = ["*/tests/*", "*/migrations/*"]
علامات pytest – تصنيف الاختبارات
import pytest
# Define marks in pyproject.toml
# [tool.pytest.ini_options]
# markers = [
# "slow: marks tests as slow",
# "integration: marks integration tests",
# "unit: marks unit tests",
# ]
@pytest.mark.slow
@pytest.mark.integration
def test_full_user_registration_flow(client, db):
# End-to-end registration test
response = client.post("/register", json={
"email": "test@example.com",
"password": "SecurePass123!"
})
assert response.status_code == 201
assert db.users.count() == 1
# Run only unit tests
# pytest -m unit
# Skip slow tests in CI
# pytest -m "not slow"
إجراءات GitHub تكامل CI
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install uv
uv pip install -e ".[dev]" --system
- name: Run tests
run: pytest --cov=app --cov-fail-under=80
- name: Upload coverage
uses: codecov/codecov-action@v4
يتطلب إتقان pytest ممارسة ولكنه يؤتي ثماره. اكتب الاختبارات جنبًا إلى جنب مع الميزات (TDD)، واستخدم التركيبات للإعداد/التفكيك، وتحديد المعلمات لحالات الحافة، والتبعيات الخارجية الوهمية. اهدف إلى تغطية أكثر من 80% لمنطق الأعمال و100% للمسارات المهمة.
🔗 Share this article
✍️ Leave a Comment