هيكِل اختبارات متينة باستخدام pytest: حقن fixtures، والتنظيف عبر yield، والاختبارات المُعامَلة على شكل جدول حالات، وفخاخ النطاقات الواسعة.
افتح هذا الدرس في Kodokonتقوم pytest على فكرتين قويّتين: الاكتشاف بالاصطلاح (ملفات test_*.py، ودوال test_*) والتعليمة assert البسيطة، التي يعرض استبطانها القيم الفعلية للطرفين عند الفشل - فلا داعي بعد الآن لحفظ assertEqual وثلاثين نسخة منها. وأداتان يجب معرفتهما فورًا: pytest.raises للتحقّق من أن استثناءً قد أُطلق فعلًا، وpytest.approx لمقارنة الأعداد العشرية دون مصارعة التقريب الثنائي.
import pytest
def divide(a: float, b: float) -> float:
return a / b
def test_nominal() -> None:
assert divide(8, 4) == 2.0
def test_zero_division() -> None:
with pytest.raises(ZeroDivisionError):
divide(1, 0)
def test_floats() -> None:
assert 0.1 + 0.2 == pytest.approx(0.3)إن fixtures هي حقن التبعيات في pytest: صرّح بوسيط يحمل اسم fixture، فتبنيها pytest ثم تحقنها. ويفصل yield بين التهيئة والتنظيف - إذ يُنفَّذ الكود الذي يليه حتى لو فشل الاختبار. ويتحكّم الوسيط scope (function افتراضيًا، أو module، أو session) في مدة حياة الكائن. ضع fixtures المشتركة في ملف conftest.py: فتصبح متاحة في المجلد كله، دون استيراد واحد.
import sqlite3
import pytest
@pytest.fixture
def db():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (name TEXT)")
yield conn
conn.close()
def test_insert(db) -> None:
db.execute("INSERT INTO users VALUES ('Ada')")
query = "SELECT name FROM users"
rows = db.execute(query).fetchall()
assert rows == [("Ada",)]يحوّل @pytest.mark.parametrize اختبارًا واحدًا إلى جدول حالات: فيصبح كل صفّ (tuple) اختبارًا مستقلًا، له اسمه ويمكن ترشيحه على حدة. كدّس عدة مزيّنات parametrize للحصول على الجداء الديكارتي للوسائط. وامنح حالاتك معرّفات id مقروءة عبر pytest.param(..., id='...') - فتقرير الفشل يجب أن يكون مفهومًا دون فتح الملف المصدري.
import pytest
def slugify(text: str) -> str:
return "-".join(text.lower().split())
@pytest.mark.parametrize(
("raw", "expected"),
[
("Hello World", "hello-world"),
(" Python rocks ", "python-rocks"),
pytest.param("", "", id="empty"),
],
)
def test_slugify(raw: str, expected: str) -> None:
assert slugify(raw) == expectedpytest -x --lf
pytest -k "slugify and not empty"
pytest -q --durations=5yield؟pytest --lf؟