كتابة اختبارات Unit Testing في Python باستخدام pytest
يمنح الاختبار الوحدوي المطور طريقة سريعة للتأكد من أن دالة صغيرة ما زالت تنتج النتيجة الصحيحة بعد التعديل. لا يعني ذلك اختبار كل سطر أو كتابة اختبارات طويلة لا يقرأها أحد. نختار وحدة محددة، ونمرر لها مدخلات معروفة، ثم نقارن النتيجة المتوقعة. يشرح المقال اختبار دالة لحساب الخصم باستخدام pytest، ثم يضيف حالات للخطأ ويستخدم fixture بسيطاً. عندما تتعود على هذا الأسلوب، ستصبح إعادة التنظيم أكثر أماناً، لأن الاختبار يكشف تغير السلوك قبل وصوله إلى المستخدم.
الفكرة الأساسية
يتكون الاختبار الجيد من تجهيز ومدخل وتنفيذ وتحقق، حتى لو كتبت هذه الخطوات في بضعة أسطر. يجب أن يكون مستقلاً عن ترتيب الاختبارات وألا يعتمد على ملف تغيره تجربة سابقة. استخدم أسماء تصف الحالة، مثل test_discount_rejects_negative_rate. إذا كانت الدالة تتصل بقاعدة بيانات أو شبكة، فافصل ذلك عن منطق الحساب أو استخدم mock عند الحاجة. الاختبار ليس لإثبات أن الكود جميل، بل لإثبات عقد واضح يمكن تغييره بقرار مقصود.
مثال برمجي عملي
# pricing.py
def final_price(price, discount_rate):
if price < 0:
raise ValueError("price cannot be negative")
if not 0 <= discount_rate <= 1:
raise ValueError("invalid discount rate")
return round(price * (1 - discount_rate), 2)
# test_pricing.py
import pytest
from pricing import final_price
def test_final_price_applies_discount():
assert final_price(100, 0.2) == 80
def test_final_price_rounds_result():
assert final_price(10, 0.15) == 8.5
def test_final_price_rejects_invalid_rate():
with pytest.raises(ValueError):
final_price(100, 1.2)
شرح المثال
تفصل الملفات بين منطق pricing والاختبارات، ثم تستورد test_pricing الدالة وتفحص ثلاث حالات. assert يقارن النتيجة، بينما pytest.raises يتأكد من أن الدالة ترفض المدخل غير الصالح بالاستثناء المتوقع. تسمية الملف تبدأ بـ test_ حتى يكتشفه pytest تلقائياً. يمكن تشغيل الاختبارات بالأمر pytest، وسيعرض التقرير أسماء الحالات الفاشلة. لا تجعل الاختبار يعتمد على الطباعة، لأن المقارنة الصريحة توضّح ما الذي يفترض أن يحدث.
ابدأ بأهم قاعدة في الدالة، ثم أضف حالة حدية وحالة خطأ. إذا فشل الاختبار، أصلح الكود أو عدل التوقع عندما يتغير العقد فعلاً، لا لمجرد إسكات الفشل. شغل الاختبارات محلياً قبل commit، وأضفها إلى عملية الدمج التلقائية. عندما تحتاج بيانات مشتركة، استخدم fixture تعيد نسخة جديدة لكل اختبار، ولا تنشئ كائناً عالمياً يتغير بين الحالات.
طريقة العمل قبل كتابة الكود
قبل فتح محرر النصوص، اكتب النتيجة التي تريد الوصول إليها وحدد المدخلات والمخرجات. يساعد هذا التمرين على كشف الحالات الغامضة مبكراً، مثل قيمة ناقصة أو قائمة فارغة أو طلب لا يصل إلى الخادم. لا تحتاج إلى مخطط كبير للمثال التعليمي، لكنك تحتاج إلى أسماء واضحة وخطوات يمكن اختبارها واحدة بعد أخرى. عندما تتغير الفكرة أثناء الكتابة، عدل التصميم قبل إضافة شروط متفرعة يصعب تتبعها.
قسّم المشكلة إلى أجزاء صغيرة، واجعل كل جزء مسؤولاً عن قرار واحد قدر الإمكان. يمكن أن تكون الأجزاء دوال، أو مكونات، أو طبقات منفصلة بحسب التقنية. لا يعني التقسيم إنشاء ملفات كثيرة، بل يعني أن تعرف أين تبحث عندما يحدث الخطأ. سجّل الافتراضات المهمة بجملة قصيرة، مثل أن القائمة مرتبة أو أن السعر غير سالب، ثم تحقق منها في المكان المناسب بدلاً من الاعتماد على الذاكرة.
اختبار المثال في حالات مختلفة
لا تختبر المسار الطبيعي فقط. جرّب مدخلاً فارغاً، وقيمة أكبر من المتوقع، وعنصراً غير موجود، وطلباً يصل من دون البيانات المطلوبة. الحالات الحدية تكشف غالباً مشكلات الفهارس والأنواع وترتيب التنفيذ. اكتب النتيجة المتوقعة قبل تشغيل الكود، ثم قارنها بالنتيجة الفعلية. إذا كان الاختبار يمر بالصدفة، أضف شرطاً أو اختباراً أوضح، ولا تكتفِ بطباعة قيمة تبدو منطقية.
- تحقق من المدخلات قبل استخدامها في الحساب أو التخزين.
- أعد رسالة مفهومة ورمزاً مناسباً عند وقوع الخطأ.
- اجعل الاختبارات قابلة للإعادة من دون اعتماد على شبكة أو وقت متغير.
- راجع أثر التغيير على الأجزاء التي تستدعي الدالة أو المكون.
ملاحظات تتعلق بجودة الكود
تظهر جودة الحل في التفاصيل الصغيرة: اسم يشرح الغرض، ودالة لا تجمع مهاماً بعيدة، ورسالة خطأ لا تترك القارئ في حيرة. لا تحاول اختصار كل سطر، فالكود المقروء أفضل من تعبير قصير يحتاج إلى شرح طويل. وفي الوقت نفسه، لا تكرر القاعدة نفسها في أماكن كثيرة؛ انقلها إلى موضع واحد عندما يكون ذلك أوضح. راجع الملف بعد أن يعمل، لأن أول نسخة تركز عادةً على الوصول إلى النتيجة أكثر من قابلية الصيانة.
احتفظ بالإعدادات التي تختلف بين جهاز وآخر خارج الكود، ولا تضع كلمات مرور أو مفاتيح خاصة في المستودع. استخدم سجلات مناسبة أثناء التطوير، ثم راجع ما ينبغي حجبه في بيئة التشغيل. إذا تعامل البرنامج مع بيانات المستخدم، فافصل بين ما يحتاجه التطبيق وما يمكن الاحتفاظ به. هذه الممارسات لا تخص لغة واحدة، بل تقلل المشكلات عندما يكبر المشروع أو يعمل عليه أكثر من شخص.
متى تعرف أن الحل يحتاج إلى تطوير؟
يحتاج المثال التعليمي إلى طبقات إضافية عندما يدخل في نظام حقيقي: قاعدة بيانات، مستخدمون متعددون، مراقبة، اختبارات، وصلاحيات. لا تضف هذه الأجزاء قبل معرفة المشكلة التي تحلها، لكن لا تنقل الكود التجريبي إلى الإنتاج كما هو. راقب حجم البيانات، وعدد الطلبات، ومصدر المدخلات، وما إذا كان الفشل يجب أن يعيد العملية أو يوقفها. اكتب قرارك في مستند صغير أو تعليق يشرح السبب، حتى لا يضطر الفريق إلى تخمينه لاحقاً.
إذا وجدت أن الخطأ يتكرر في أكثر من مكان، فابحث عن قاعدة مشتركة. وإذا أصبح التعديل في ملف صغير يؤثر في ملفات كثيرة، فراجع حدود المسؤوليات. لا توجد بنية واحدة صحيحة لكل مشروع، لكن توجد أسئلة تساعدك على اختيار بنية مناسبة: من يملك البيانات؟ من يغيرها؟ ماذا يحدث عند الفشل؟ وكيف يمكن اختبار الجزء من دون تشغيل النظام كله؟
أخطاء ينبغي تجنبها
من الأخطاء اختبار تفاصيل داخلية بدلاً من النتيجة التي يهم المستخدم الحصول عليها، أو كتابة اختبار يمر دائماً لأنه لا يحتوي assert. لا تستخدم وقتاً عشوائياً أو شبكة حقيقية في اختبار وحدوي إذا كان المطلوب اختبار دالة محلية. يبالغ بعض المطورين في استخدام mock حتى لا يختبروا السلوك الفعلي، ثم يكتشفون أن التكامل مكسور. اجمع بين اختبارات الوحدات واختبارات التكامل عند الحاجة.
خطوة تالية مناسبة
أضف parametrization لاختبار عدة أسعار ومعدلات، ثم تعلم fixtures النطاقية وقياس التغطية. اكتب اختباراً يفشل أولاً عند إضافة قاعدة جديدة، ثم نفذ القاعدة. هذا الإيقاع يجعل الاختبار جزءاً من التصميم، لا مهمة تأتي بعد الانتهاء من كل شيء.
اجعل الاختبارات مستقلة
يفضل أن يبدأ كل اختبار من حالة يعرفها، ثم ينظف ما أنشأه بعد النهاية. لا تجعل اختباراً يعتمد على ترتيب اختبار آخر أو على ملف مؤقت بقي من تشغيل سابق. إذا احتاجت الدالة إلى الوقت أو الشبكة، مرر الاعتماد إليها أو استخدم بديلاً في الاختبار. بهذه الطريقة يصبح الفشل قابلاً للإعادة على جهازك وعلى خادم التكامل المستمر. لا تكتب اختباراً يكرر تنفيذ التطبيق كاملاً عندما يكون المطلوب فحص دالة صغيرة. الاختبار الجيد يشرح السلوك المتوقع، ويعطي رسالة فشل تساعدك على تحديد السبب، ولا ينهار لمجرد تغير ترتيب مفاتيح قاموس غير مهم.
قراءة نتيجة الاختبار
لا تتعامل مع نجاح جميع الاختبارات كدليل على خلو البرنامج من المشكلات. راجع المسارات التي لم تغطها الاختبارات، وحدد الحالات التي يصعب إعادة إنتاجها. أضف اختباراً عندما تصلح خطأ مهماً، لأن الاختبار يصبح توثيقاً يمنع عودة المشكلة. ومع الوقت، راقب سرعة المجموعة كاملة حتى لا يتحول الاختبار إلى خطوة بطيئة يتجنبها الفريق.
الخلاصة
أضف parametrization لاختبار عدة أسعار ومعدلات، ثم تعلم fixtures النطاقية وقياس التغطية. اكتب اختباراً يفشل أولاً عند إضافة قاعدة جديدة، ثم نفذ القاعدة. هذا الإيقاع يجعل الاختبار جزءاً من التصميم، لا مهمة تأتي بعد الانتهاء من كل شيء. يوضح هذا الموضوع كيف يتحول مفهوم نظري إلى خطوات يمكن تشغيلها وفحصها.
ابدأ بتطبيق المثال على ملف صغير، ثم غيّر مدخلاً واحداً وراقب النتيجة. بعد ذلك أضف حالة فشل واكتب اختباراً لها، ثم انقل الفكرة إلى مشروعك الحقيقي بحذر. عندما تفهم سبب كل خطوة، ستستطيع تغيير الأدوات أو اللغة من دون فقدان المفهوم. البرمجة تتحسن بالمحاولات القصيرة والمراجعة المستمرة، لا بنسخ كود طويل من دون معرفة ما الذي يحميه أو ما الذي قد يكسره.