تخيل معايا: إنت بتكتب unit test (اختبار وحدة) لكود جديد، الاختبار فشل، وطلعلك رسالة AssertionError بتقولك "الاتنين مش متساوين" - بس! مش عارف إيه المشكلة بالضبط، ولا فين الاختلاف!
محبط، صح؟ طب تخيل لو الرسالة بتقولك بالضبط إيه العناصر المختلفة في القائمتين، وفين الاختلاف حصل!
ده بالضبط اللي Pytest بيعمله - بس السؤال: إزاي؟!
في بايثون (وفي لغات برمجة كتير)، فيه كلمة محجوزة اسمها assert - شغلتها بسيطة:
وظيفتها:

يعني إيه بالعربي؟ لو بتختبر إن نتيجة دالة (function) معينة صحيحة، بتستخدم assert:
assert calculate_total([100, 200, 300]) == 600 # لازم يطلع 600
المشكلة: لو الاختبار فشل، الرسالة مش بتقولك إيه المشكلة بالضبط!
تخيل: عندك قائمتين (lists) عايز تتأكد إنهم متطابقين:

الرسالة بتقول: "AssertionError" - وبس!
طب شوف بقى لما نستخدم Pytest في نفس الكود:

ياه! دي رسالة مفيدة فعلاً:
السؤال الكبير: إزاي Pytest بيعمل كده؟! هو قدر يغير سلوك كلمة محجوزة في بايثون؟!
السؤال ده ودّاني في رحلة عشان أفهم إزاي Pytest بيشتغل!
الفكرة: الطريقة الوحيدة إن Pytest يديك رسائل مفصلة كده، هي إنه يغير طريقة عمل الـ assert نفسها!
المشكلة: بعد بحث سريع، هتلاقي إن مفيش طريقة تعيد تعريف الكلمات المحجوزة (keywords) في بايثون!
unittest (مكتبة الاختبارات القديمة في بايثون):

يعني: unittest استسلمت وعملت حل بديل!
طب Pytest؟ Pytest لسه بيستخدم assert العادي - إزاي بقى يهرب من القيد ده؟
الإجابة: لازم نفهم حاجة اسمها Abstract Syntax Tree (AST)!
تخيل معايا لما بايثون بتشغل الكود بتاعك:
بايثون بتقسم الكود لـ رموز صغيرة:
if، for، whilex، total، user_name3.14، 42، 100==، +، *الرموز دي بتتنظم في شجرة بتوضح:
مثال: شوف المعادلتين دول:

1 + 2 * 3 # النتيجة: 7
(1 + 2) * 3 # النتيجة: 9
ليه النتايج مختلفة؟ لأن الـ AST مختلف!
يعني: الـ AST هي اللي بتحدد ترتيب تنفيذ الكود حسب قواعد اللغة!
الشجرة دي بتتحول لـ bytecode (كود بايت) - ده اللي بايثون بتفهمه فعلاً وبتشغله.
ده المفتاح: بايثون بتديك hooks (نقاط دخول) في عملية import (استيراد الملفات)!
معنى كده:
ده بالضبط اللي Pytest بيعمله! شوف جزء من الكود الأصلي لـ Pytest:

باختصار: Pytest بيعمل كده:
والجزء الجميل: بايثون جاهزة بكده! فيه module مدمج اسمه ast بيساعدك تعمل كل ده!
خلينا نشوف بالكود إزاي نلعب بالـ AST:
import ast
code = "x = 1 + 2 * 3"
tree = ast.parse(code) # حول الكود لشجرة
print(ast.dump(tree)) # اطبع الشجرة بشكل قابل للقراءة

بايثون بتوفرلك ast.NodeTransformer - class جاهز يساعدك تعدل الشجرة بسهولة!
يعني: مش محتاج تكتب كود معقد عشان تعبر الشجرة - بايثون مجهزاك!
تخيل معايا: عايزين نعمل حاجة شبه Pytest - بس بدل ما نعدل assert، هنعمل حاجة أبسط وأوضح!
الفكرة: هنعمل كود يقرب الأرقام تلقائياً في الاختبارات!
عندنا ملف فيه اختبار بسيط:

def test_equal():
assert 3.14159 == 3.14 # دي هتفشل - الرقمين مش متطابقين!
المشكلة: الاختبار ده هيفشل لأن 3.14159 مش بالضبط 3.14!
لو استوردنا my_test.py عادي:

from my_test import test_equal
test_equal() # AssertionError - فشل!
النتيجة: ❌ AssertionError - فشل!
الخطة:
my_test.pyالكود: دالة بتعدل الـ AST:

شرح الكود:
round()الكود: hook للاستيراد:

شرح الـ Hook:
my_test.pyauto_round عشان يعدل الكودالنتيجة النهائية:
# الكود الأصلي كان:
assert 3.14159 == 3.14
# بعد التعديل بقى:
assert round(3.14159) == round(3.14)
# النتيجة: ✅ نجح! - لأن الاتنين بيطلعوا 3
رحلة اليوم علمتنا:
الخلاصة: Pytest مش سحر - هو استغلال ذكي لميزات بايثون المدمجة!
تقدر تشوف الكود الكامل هنا لو عايز تجربه بنفسك!
طارق عمرو، 13 يناير 2023