الاختبارات الوحدوية
درس 1 من 3
1الاختبارات الوحدوية
الصورة الكبيرةالاختبار كود يتأكد أن كودك يعمل، ويبقى يتأكد كل مرة تغيّر شيئًا. بدونه كل تعديل مقامرة: تصلح شيئًا هنا وتكسر شيئًا هناك دون أن تعرف.
ماذا تشاهد في الرسم: كل اختبار ثلاث خطوات: جهّز المدخلات، استدعِ الدالة، وتحقق من النتيجة. أخضر يعني نجح، وأحمر يعني أن شيئًا تغيّر.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
مثل قائمة فحص الطيار قبل الإقلاع: نفس الأسئلة كل مرة، والجواب يجب أن يكون نفسه. لو تغيّر جواب واحد تعرف فورًا أين المشكلة قبل أن تطير.
الاختبار الوحدوي: سريع (ميلي ثوانٍ)، معزول (بلا شبكة أو قاعدة بيانات أو ساعة حقيقية)، حتمي، ويفحص سلوكًا لا تفاصيل تنفيذ. pytest يكتشف دوال test_* ويعرض فروق assert بوضوح، وpytest.mark.parametrize لجدول حالات، وpytest.raises للأخطاء، والـ fixtures للتجهيز المشترك. البدائل الوهمية (unittest.mock) تستبدل الاعتمادية الخارجية، لكن الإكثار منها يجعل الاختبار يفحص الـ mock لا الكود. التغطية (coverage) مؤشر لما لم يُختبر، لا هدف بحد ذاته.
قبل أن تبدأ
🐍الدوال والاستثناءات
تكتب دالة وترفع خطأ بـ raise.
الفكرة الجوهرية
1اختبار بثلاث خطوات
هذه دالة خصم واختباراتها. نشغلها بـ unittest ونطبع ملخصًا ثابتًا (pytest يكتشفها بنفس الأسماء):
import io
import unittest
def apply_discount(price, percent):
if not 0 <= percent <= 100:
raise ValueError("percent must be 0-100")
return round(price * (100 - percent) / 100, 2)
class DiscountTests(unittest.TestCase):
def test_regular_discount(self):
price, percent = 50, 20 # arrange
result = apply_discount(price, percent) # act
self.assertEqual(result, 40) # assert
def test_zero_and_full(self):
self.assertEqual(apply_discount(50, 0), 50)
self.assertEqual(apply_discount(50, 100), 0)
def test_rejects_bad_percent(self):
with self.assertRaises(ValueError):
apply_discount(50, 120)
suite = unittest.defaultTestLoader.loadTestsFromTestCase(DiscountTests)
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
print("ran:", result.testsRun, "failures:", len(result.failures), "ok:", result.wasSuccessful())بنفس الفكرة في pytest، أقصر:
import pytest
from shop import apply_discount
@pytest.mark.parametrize("price, percent, expected", [
(50, 20, 40), (50, 0, 50), (50, 100, 0), (19.99, 15, 16.99),
])
def test_apply_discount(price, percent, expected):
assert apply_discount(price, percent) == expected
def test_rejects_bad_percent():
with pytest.raises(ValueError):
apply_discount(50, 120)2ما الذي تختبره؟
الحالة العادية، ثم الأطراف: صفر، فارغ، أكبر قيمة، سالب، None، ونص بالعربية. ثم الأخطاء المتوقعة. وكل خطأ أصلحته يحصل على اختبار باسمه حتى لا يعود.
assert. جرّب كسر الكود عمدًا: إن بقي الاختبار أخضر فهو لا يحميك.3البدائل الوهمية
دالة ترسل رسالة SMS لا يجب أن ترسل رسالة حقيقية في كل اختبار. نستبدل المرسل ببديل يسجّل ما طُلب منه:
from unittest.mock import Mock
def notify_ready(order, sms):
if order["status"] != "ready":
return False
sms.send(order["phone"], f"Order {order['id']} is ready")
return True
sms = Mock()
print(notify_ready({"id": 7, "status": "ready", "phone": "0500000000"}, sms))
sms.send.assert_called_once_with("0500000000", "Order 7 is ready")
print(notify_ready({"id": 8, "status": "new", "phone": "0500000000"}, sms))
print("send calls:", sms.send.call_count)مثال محلول
المسألة: تقرير يقول «الفاتورة بخصم 15% على 19.99 أعطت 16.991». الاختبار أولًا: assert apply_discount(19.99, 15) == 16.99 يفشل. الإصلاح: round(..., 2). النتيجة: الاختبار يمر، ويبقى في الملف ليحرس هذا الخطأ للأبد.
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| الاختبارات تبطئ التطوير. | تبطئ أول ساعة، وتسرّع كل تعديل بعدها لأنك تعرف فورًا إن كسرت شيئًا. |
| تغطية 100% تعني لا أخطاء. | تعني أن كل سطر نُفّذ، لا أن كل حالة فُحصت. |
اختبر فهمك
ما الذي يجعل الاختبار وحدويًا؟
العزل يجعله سريعًا وثابت النتيجة.
كيف تعرف أن اختبارك يحميك فعلًا؟
اختبار لا يحمرّ أبدًا لا يفحص شيئًا.
متى تستخدم mock؟
البديل يمنع الآثار الحقيقية ويُبقي الاختبار سريعًا.
بطاقات مراجعة سريعة
تحقق من فهمك
لماذا نمرّر الاعتمادية كوسيط؟
ليسهل استبدالها ببديل في الاختبار دون تعديل الكود.
ما الذي لا تختبره؟
تفاصيل التنفيذ الداخلية ومكتبات الآخرين. اختبر السلوك الذي يراه المستخدم.
كم اختبارًا لكل دالة؟
اختبار لكل سلوك: الحالة العادية، كل طرف مهم، وكل خطأ متوقع.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.