الـ Assert في Pytest مش اللي إنت فاكره!

إيه هو AST؟ وإزاي Pytest بيهاكه عشان يديك رسائل خطأ أحسن؟

تخيل معايا: إنت بتكتب unit test (اختبار وحدة) لكود جديد، الاختبار فشل، وطلعلك رسالة AssertionError بتقولك "الاتنين مش متساوين" - بس! مش عارف إيه المشكلة بالضبط، ولا فين الاختلاف!

محبط، صح؟ طب تخيل لو الرسالة بتقولك بالضبط إيه العناصر المختلفة في القائمتين، وفين الاختلاف حصل!

ده بالضبط اللي Pytest بيعمله - بس السؤال: إزاي؟!

الـ Assert العادي في بايثون: بسيط بس مش مفيد أوي

في بايثون (وفي لغات برمجة كتير)، فيه كلمة محجوزة اسمها assert - شغلتها بسيطة:

وظيفتها:

عبارة assert في بايثون

يعني إيه بالعربي؟ لو بتختبر إن نتيجة دالة (function) معينة صحيحة، بتستخدم assert:

assert calculate_total([100, 200, 300]) == 600  # لازم يطلع 600

المشكلة: لو الاختبار فشل، الرسالة مش بتقولك إيه المشكلة بالضبط!

مثال يوضح المشكلة

تخيل: عندك قائمتين (lists) عايز تتأكد إنهم متطابقين:

رسائل خطأ مش وصفية من assert العادي

الرسالة بتقول: "AssertionError" - وبس!

طب شوف بقى لما نستخدم Pytest في نفس الكود:

رسائل خطأ مفصلة من Pytest

ياه! دي رسالة مفيدة فعلاً:

السؤال الكبير: إزاي Pytest بيعمل كده؟! هو قدر يغير سلوك كلمة محجوزة في بايثون؟!

هل ممكن نعيد تعريف كلمة محجوزة في بايثون؟

السؤال ده ودّاني في رحلة عشان أفهم إزاي Pytest بيشتغل!

الفكرة: الطريقة الوحيدة إن Pytest يديك رسائل مفصلة كده، هي إنه يغير طريقة عمل الـ assert نفسها!

المشكلة: بعد بحث سريع، هتلاقي إن مفيش طريقة تعيد تعريف الكلمات المحجوزة (keywords) في بايثون!

مكتبات تانية عملت إيه؟

unittest (مكتبة الاختبارات القديمة في بايثون):

Unittest بتعمل methods خاصة بدل assert

يعني: unittest استسلمت وعملت حل بديل!

طب Pytest؟ Pytest لسه بيستخدم assert العادي - إزاي بقى يهرب من القيد ده؟

الإجابة: لازم نفهم حاجة اسمها Abstract Syntax Tree (AST)!

الشجرة النحوية المجردة (AST): إزاي بايثون بتفهم الكود؟

تخيل معايا لما بايثون بتشغل الكود بتاعك:

الخطوة الأولى: تقسيم لـ Tokens (رموز)

بايثون بتقسم الكود لـ رموز صغيرة:

الخطوة التانية: تنظيم في شجرة (AST)

الرموز دي بتتنظم في شجرة بتوضح:

مثال: شوف المعادلتين دول:

مثال AST مع المعادلات

1 + 2 * 3  # النتيجة: 7
(1 + 2) * 3  # النتيجة: 9

ليه النتايج مختلفة؟ لأن الـ AST مختلف!

يعني: الـ AST هي اللي بتحدد ترتيب تنفيذ الكود حسب قواعد اللغة!

الخطوة التالتة: تحويل لـ Bytecode

الشجرة دي بتتحول لـ bytecode (كود بايت) - ده اللي بايثون بتفهمه فعلاً وبتشغله.

الحيلة السحرية: بايثون بتسمحلك تعدل الـ AST!

ده المفتاح: بايثون بتديك hooks (نقاط دخول) في عملية import (استيراد الملفات)!

معنى كده:

ده بالضبط اللي Pytest بيعمله! شوف جزء من الكود الأصلي لـ Pytest:

كود Pytest الأصلي

باختصار: Pytest بيعمل كده:

  1. لما يستورد ملف الاختبار بتاعك
  2. بيقرا الـ AST بتاعه
  3. بيلاقي كل assert في الكود
  4. بيستبدلها بـ if statement مع كود بيطبع تفاصيل الخطأ!

والجزء الجميل: بايثون جاهزة بكده! فيه module مدمج اسمه ast بيساعدك تعمل كل ده!

إزاي نستخدم وحدة AST في بايثون؟

خلينا نشوف بالكود إزاي نلعب بالـ AST:

خطوة 1: تحويل كود لـ AST

import ast

code = "x = 1 + 2 * 3"
tree = ast.parse(code)  # حول الكود لشجرة

خطوة 2: عرض الشجرة

print(ast.dump(tree))  # اطبع الشجرة بشكل قابل للقراءة

عرض AST الكود

خطوة 3: تعديل الشجرة

بايثون بتوفرلك ast.NodeTransformer - class جاهز يساعدك تعدل الشجرة بسهولة!

يعني: مش محتاج تكتب كود معقد عشان تعبر الشجرة - بايثون مجهزاك!

وقت التطبيق: نعمل Pytest مصغر!

تخيل معايا: عايزين نعمل حاجة شبه Pytest - بس بدل ما نعدل assert، هنعمل حاجة أبسط وأوضح!

الفكرة: هنعمل كود يقرب الأرقام تلقائياً في الاختبارات!

الملف الأول: my_test.py

عندنا ملف فيه اختبار بسيط:

ملف my_test.py

def test_equal():
    assert 3.14159 == 3.14  # دي هتفشل - الرقمين مش متطابقين!

المشكلة: الاختبار ده هيفشل لأن 3.14159 مش بالضبط 3.14!

الملف التاني: tester.py (بدون تعديل)

لو استوردنا my_test.py عادي:

ملف tester.py بدون تعديل

from my_test import test_equal
test_equal()  # AssertionError - فشل!

النتيجة: ❌ AssertionError - فشل!

الحل: نعدل الـ AST قبل الاستيراد!

الخطة:

  1. نعترض عملية الاستيراد (import)
  2. نقرا محتوى my_test.py
  3. نعمل AST للكود
  4. نستبدل كل الأرقام العشرية بـ round() - يقرب الرقم
  5. نحفظ الـ AST المعدل ونشغله!

الكود: دالة بتعدل الـ AST:

دالة auto_round في tester.py

شرح الكود:

  1. ast.parse: حول الكود لـ AST
  2. NodeTransformer: عدّل كل رقم عشري → حطه جوا round()
  3. ast.unparse: حول الـ AST رجوع لكود عادي

الكود: hook للاستيراد:

Hook الاستيراد في tester.py

شرح الـ Hook:

النتيجة النهائية:

# الكود الأصلي كان:
assert 3.14159 == 3.14

# بعد التعديل بقى:
assert round(3.14159) == round(3.14)

# النتيجة: ✅ نجح! - لأن الاتنين بيطلعوا 3

الخلاصة: فهمنا إيه؟

رحلة اليوم علمتنا:

1. المشكلة الأصلية

2. الحل السحري: AST

3. القوة الخفية في بايثون

4. Pytest استغل القوة دي

5. التطبيق العملي

الخلاصة: Pytest مش سحر - هو استغلال ذكي لميزات بايثون المدمجة!

تقدر تشوف الكود الكامل هنا لو عايز تجربه بنفسك!


طارق عمرو، 13 يناير 2023

الترجمات: [EN], [NL]