لا يقتصر البرمجة (programming) على كتابة التعليمات البرمجية فقط. من المهم التحقق من أن التعليمات البرمجية تفعل ما ينبغي عليها فعله. تسمى عملية التحقق من أن البرنامج يعمل كما هو متوقع الاختبار (testing).
ربما قمت بالفعل باختبار برامجك عن طريق تنفيذها. عندما تختبر برنامجك، عادةً ما تدخل بعض بيانات الإدخال وتطبع النتيجة إذا كانت صحيحة.
هذا جيد لبرنامج صغير، لكنه يصبح أصعب كلما كبر البرنامج. تحتوي البرامج الأكبر على المزيد من الخيارات لما يمكنها فعله بناءً على الاحتمالات إدخال وتكوين المستخدم. يصبح اختبارها اليدوي يستغرق وقتًا طويلاً، خاصة عندما يحتاج إلى التكرار بعد كل تغيير، ويصبح أكثر من المحتمل أن تتسلل الأخطاء دون أن يلاحظها أحد إلى التعليمات البرمجية الخاصة بنا.
البشر ليسوا جيدين جدًا في أداء المهام المتكررة المملة، فهذا هو مجال الحواسيب. وليس من المستغرب أن هذا هو السبب وراء قيام المطورين بكتابة التعليمات البرمجية التي تتحقق من برامجهم.
حتى الآن، استخدمنا فقط الوحدات (modules) التي تأتي مثبتة مع بايثون،
على سبيل المثال، وحدات مثل math
أو turtle
.
هناك العديد من المكتبات (libraries) الأخرى التي لم يتم تضمينها في بايثون
ولكن يمكنك تثبيتها في بيئة بايثون الخاصة بك واستخدامها.
تسمى مكتبة الاختبار في بايثون unittest
.
من الصعب جدًا استخدام هذه المكتبة لذا سنستخدم مكتبة أفضل.
سنقوم بتثبيت مكتبة pytest
التي هي أسرع وأسهل في الاستخدام وشائعة جدًا.
أرسل الأمر التالي. (إنه أمر سطر أوامر - command-line command،
تمامًا مثل cd
أو mkdir
؛ لا تدخله في وحدة تحكم بايثون - Python console.)
$ python -m pip install pytest
ما هو pip ولماذا نستخدمه؟
pip
هي أداة سطر أوامر بايثون (Python command-line tool) لتثبيت مكتبات الطرف الثالث (3rd-party)
مكتبات بايثون من فهرس حزم بايثون (PyPI)
ومصادر أخرى (مثل مستودعات Git).
python -m pip install pytest
يجعل بايثون يقوم بتثبيت مكتبة pytest
من PyPI.
للحصول على مساعدة حول كيفية استخدام pip، قم بتشغيل python -m pip --help
.
`
<python -m <command
يخبر بايثون بتنفيذ نص (script) من
وحدة بايثون (Python module) المسماة <command>
(على سبيل المثال، python -m pip ...
).
في بيئة بايثون مهيأة بشكل صحيح، يجب أن يكون من الممكن استدعاء
<command>
مباشرة، بدون مساعدة أمر python
(على سبيل المثال، pip ...
)
لتوفير عناء التعقيدات غير الضرورية مع بيئة بايثون قد تكون
مهيأة بشكل خاطئ، نوصي باستخدام النسخة الأطول
<python -m <command
.
سنعرض الاختبار من خلال مثال بسيط للغاية.
هناك دالة (function) add
يمكنها جمع رقمين.
هناك دالة أخرى تختبر ما إذا كانت
ترجع الدالة add
نتائج صحيحة لأرقام محددة.
انسخ الكود إلى ملف باسم test_addition.py
في مجلد فارغ جديد.
يعد تسمية الملفات ودوال الاختبار مهمًا بالنسبة لـ pytest
(بالإعدادات الافتراضية (default settings)).
من المهم أن تبدأ أسماء الملفات التي تحتوي على الاختبارات ودوال الاختبار بـ test_
.
def add(a, b):
return a + b
def test_add():
assert add(1, 2) == 3
تسمية الملفات ودوال الاختبار مهمة
يقوم pytest
بفحص التعليمات البرمجية الخاصة بك و
يبحث عن الاختبارات المضمنة. عند العثور عليها، يتم تنفيذ هذه الاختبارات.
بشكل افتراضي، يجب أن تبدأ أسماء ملفات الاختبار ودوال الاختبار بـ
البادئة test_
ليتم التعرف عليها كاختبارات.
ماذا تفعل دالة الاختبار؟
يقوم بيان assert
بتقييم التعبير الذي يليه.
إذا كانت النتيجة غير صحيحة، فإنه يثير استثناء AssertionError
الذي يفسره pytest
على أنه اختبار فاشل.
يمكنك أن تتخيل أن assert a == b
يفعل ما يلي:
if not (a == b):
raise AssertionError
لا تستخدم assert
خارج دوال الاختبار في الوقت الحالي.
بالنسبة للتعليمات البرمجية "العادية"، فإن assert
لديه وظائف
لن نشرحها الآن.
تقوم بتنفيذ الاختبارات باستخدام الأمر <python -m pytest -v <path
متبوعًا بمسار الملف الذي يحتوي على الاختبارات.
يمكنك حذف <filename>
و بذلك فان الامرpython -m pytest -v
يفحص المجلد الحالي ويقوم بتشغيل الاختبارات في جميع الملفات التي تبدأ أسماؤها بـ
البادئة test_
.
يمكنك أيضًا استخدام مسار إلى مجلد حيث يجب أن يبحث pytest
عن
الاختبارات.
يفحص هذا الأمر الملف المحدد ويستدعي جميع الدوال التي تبدأ
بالبادئة test_
. يقوم بتنفيذها ويتحقق مما إذا كانت تثير أي استثناء،
على سبيل المثال، تم إثارته بواسطة بيان assert
.
$ python3 -m pytest -v test_addition.py
============================= test session starts ==============================
platform linux -- Python 3.8.3, pytest-7.1.2, pluggy-1.0.0
rootdir: /tmp/test_example
collected 1 item
test_addition.py . [100%]
============================== ␐[1m1 passed␐[0m in 0.00s␐[0m ===============================
في حالة حدوث استثناء، يعرض pytest
رسالة حمراء مع
تفاصيل إضافية يمكن أن تساعدك في العثور على الخطأ وإصلاحه:
============================= test session starts ==============================
platform linux -- Python 3.8.3, pytest-7.1.2, pluggy-1.0.0
rootdir: /tmp/test_example
collected 1 item
test_addition.py F [100%]
=================================== FAILURES ===================================
___________________________________ test_add ___________________________________
def test_add():
> assert add(1, 2) == 3
E assert 4 == 3
E + where 4 = add(1, 2)
test_addition.py␐[0m:5: AssertionError
=========================== short test summary info ============================
FAILED test_addition.py::test_add - assert 4 == 3
============================== ␐[1m1 failed␐[0m in 0.01s␐[0m ===============================
حاول تشغيل الاختبار بنفسك. قم بتعديل الدالة add
أو (اختبارها) بحيث
يفشل الاختبار.
عادةً لا تكتب الاختبارات في نفس الملف مع التعليمات البرمجية العادية. عادةً ما تكتب الاختبارات في ملف آخر. بهذه الطريقة، يكون الكود الخاص بك أسهل في القراءة، ويجعل من الممكن توزيع الكود فقط، بدون الاختبارات، على شخص مهتم فقط بتنفيذ البرنامج.
قسّم ملف test_addition.py
: انقل الدالة add
إلى وحدة جديدة addition.py
.
في ملف test_addition.py
، احتفظ بالاختبار فقط.
إلى ملف test_addition.py
، أضف from addition import add
في الأعلى
حتى يتمكن الاختبار من استدعاء الدالة المختبرة.
يجب أن ينجح الاختبار مرة أخرى.
هيا نحاول الآن إضافة اختبارين مختلفين لدالة لحساب محيط مستطيل من دوال مخصصة
def find_perimeter(width, height):
"Returns the rectangle's perimeter of the given sides"
return 2 * (width + height)
print(find_perimeter(2, 4)) # this is how you'd normally check result without "testing"
الاختبارات الآلية (Automated tests) هي دوال تتحقق، بدون تدخل يدوي، من أن جميع ميزات البرنامج المختبر تعمل بشكل صحيح. لا يمنحنا الاختبار دليلًا بنسبة 100٪ على أن الكود خالٍ من الأخطاء ولكنه لا يزال أفضل من عدم الاختبار على الإطلاق.
تجعل الاختبارات الآلية تعديل الكود أسهل حيث يمكنك العثور على الأخطاء المحتملة في الوظائف الموجودة بشكل أسرع (المعروفة باسم التراجعات - regressions).
يجب أن تكون الاختبارات الآلية قادرة على التشغيل دون مراقبة. غالبًا ما يتم تنفيذها تلقائيًا ويتم الإبلاغ عن حالات الفشل عبر نوع من الإشعارات، على سبيل المثال، عن طريق البريد الإلكتروني.
مثال ل(repository) بايثون مع pytest.
من الناحية العملية، هذا يعني أن الاختبارات يجب ألا تعتمد على تفاعل مباشر
مع المستخدم، على سبيل المثال، دالة input
لن تعمل في الاختبارات.
هل يمكننا اختبار تفاعل المستخدم في الاختبارات الآلية؟
هناك تقنيات اختبار تسمح لنا بمحاكاة تفاعل المستخدم في واجهات المستخدم. لكن هذا خارج نطاق هذه الدورة.
قد يجعل هذا عملك أصعب في بعض الأحيان. لنلقِ نظرة على مشروع أكثر تعقيدًا، لعبة XO أحادية البعد (1D tic-tac-toe).
إذا لم يكن لديك برنامج XO أحادي البعد، فإن الأقسام التالية هي نظرية فقط.
إذا كنت تدرس في المنزل، فأكمل درس XO أحادي البعد قبل المتابعة. وصف المهمة موجود في XO أحادي البعد
يبدو هيكل كود XO أحادي البعد تقريبًا كما يلي:
import random # (and possibly other import statements that are needed)
# (وربما عبارات استيراد أخرى ضرورية)
def move(board, space_number, mark):
"""Returns the board with the specified mark placed in the specified position"""
# تُرجع اللوحة مع العلامة المحددة الموضوعة في الموضع المحدد
...
def player_move(board):
"""Asks the player what move should be done and returns the board
with the move played.
"""
# تسأل اللاعب عن الحركة التي يجب القيام بها وتُرجع اللوحة
# مع الحركة التي تم لعبها.
...
input('What is your move? ')
...
def computer_move(board):
"""Places computer mark on random empty position and returns the board
with the move played.
"""
# تضع علامة الكمبيوتر في موضع فارغ عشوائي وتُرجع اللوحة
# مع الحركة التي تم لعبها.
...
def tic_tac_toe_1d():
"""Starts the game
It creates an empty board and runs player_move and computer_move alternately
until the game is finished.
"""
# تبدأ اللعبة
# تقوم بإنشاء لوحة فارغة وتشغل player_move و computer_move بالتناوب
# حتى تنتهي اللعبة.
while ...:
...
player_move(...)
computer_move(...)
...
# Start the game:
# ابدأ اللعبة:
tic_tac_toe_1d()
كما وصفنا في درس الوحدات (modules)، إذا قمت باستيراد هذه الوحدة (module)، فإن بايثون تنفذ جميع الأوامر الموجودة فيها، من الأعلى إلى الأسفل:
يقوم الأمر الأول، import
، بتهيئة المتغيرات والدوال الخاصة بـ
وحدة random
. إنها وحدة من مكتبة بايثون القياسية ومن غير المحتمل
أن يكون لها أي تأثير جانبي يدعو للقلق.
تعريفات الدوال (بيانات def
وكل ما بداخلها)
تقوم فقط بتعريف الدوال ولكنها لا تنفذها.
يبدأ استدعاء الدالة tic_tac_toe_1d
اللعبة.
تستدعي tic_tac_toe_1d
الدالة ()player_move
التي تستدعي ()input
.
هذه مشكلة.
إذا قمت باستيراد هذه الوحدة (module) إلى الاختبارات، فإن input
يفشل ولا يتم
استيراد الوحدة.
إذا كنت ترغب في استيراد مثل هذه الوحدة من مكان آخر، على سبيل المثال، كنت ترغب
في استخدام ()move
في لعبة مختلفة، فإن استيراد الوحدة نفسها سيبدأ
لعبة XO أحادية البعد!
يعد استدعاء tic_tac_toe_1d
تأثيرًا جانبيًا ونحتاج إلى إزالته.
حسنًا، لكن لا يمكنك بدء اللعبة بدونها! هناك طريقتان لإصلاح ذلك.
يمكننا إنشاء ملف بايثون جديد فقط لتشغيل اللعبة، بينما ستبقى الدوال في الملف القديم.
على سبيل المثال، قم بإنشاء ملف جديد باسم game.py
وننقل استدعاء ()tic_tac_toe_1d
إليه:
import tic_tac_toe
tic_tac_toe.tic_tac_toe_1d()
لا يمكنك اختبار هذه الوحدة لأنها تستدعي input
بشكل غير مباشر.
ولكن يمكنك تنفيذه إذا كنت ترغب في اللعب كـ python game.py
يمكنك استيراد الوحدة الأصلية في ملفات الاختبار أو الوحدات الأخرى بدون آثار جانبية.
يمكن أن يبدو اختبار الوحدة الأصلية كما يلي:
import tic_tac_toe
def test_move_to_empty_space():
board = tic_tac_toe.computer_move('--------------------')
assert len(board) == 20
assert board.count('x') == 1
assert board.count('-') == 19
هناك طريقة خاصة للتحقق مما إذا كانت بايثون تستورد فقط الدوال (functions) من ملف أو إذا كانت تنفذه مباشرةً.
من الممكن ذلك عن طريق مقارنة قيمة متغير (variable) "سحري" __name__
.
يتوفر المتغير (variable) __name__
في أي وقت تقوم فيه بتشغيل برنامج بايثون (Python program)، وإذا كانت قيمته
__main__
، فقد تم تشغيله من النص الرئيسي (main script). وإذا لم يكن كذلك، فقد تم استيراده فقط.
if __name__ == "__main__":
tic_tac_toe_1d()
الآن يمكنك استيراد الوحدة (module) الأصلية في ملفات الاختبار (test files) أو وحدات أخرى (other modules) بدون آثار جانبية (side effects) وتشغيلها للعب اللعبة.