المعلمة من الملف في py.test

في مجال الاختبار التلقائي ، يمكنك العثور على أدوات متنوعة ، على سبيل المثال ، py.test هو أحد أكثر الحلول شعبية لكتابة الاختبارات التلقائية في Python.


بعد الاطلاع على الكثير من الموارد المتعلقة بـ pytest ودراسة الوثائق من الموقع الرسمي للمشروع ، لم أتمكن من العثور على وصف مباشر للحل لأحد المهام الرئيسية - تشغيل الاختبارات مع اختبار البيانات المخزنة في ملف منفصل. خلاف ذلك ، يمكن القول ، تحميل المعلمات في وظائف الاختبار من الملف (الملفات) أو المعلمة من الملف مباشرة. لم يتم وصف هذا الإجراء في أي مكان في التعقيدات والإشارة الوحيدة لهذه الميزة موجودة في سطر واحد فقط من وثائق pytest.


في هذه المقالة سوف أتحدث عن حل لهذه المشكلة.




مهمة


وتتمثل المهمة الرئيسية في إنشاء حالات اختبار في شكل معلمات test_input و expected_result في كل وظيفة اختبار فردية من أسماء وظائف الملف المقابلة.


مهام إضافية:


  • اختيار تنسيق قابل للقراءة من قبل الإنسان للملفات مع حالات اختبار ؛
  • ترك القدرة على دعم الحالات اختبار الثابت ترميز ؛
  • عرض معرفات واضحة لكل حالة.

أدوات


في المقالة ، أستخدم Python 3 (2.7 مناسب أيضًا) و pyyaml ​​و pytest (الإصدارات 5+ لـ Python 3 أو 4.6 لـ Python 2.7) دون استخدام الإضافات الخارجية. بالإضافة إلى ذلك ، سيتم استخدام مكتبة os القياسية.


يجب هيكلة الملف نفسه الذي سنأخذ منه حالات الاختبار باستخدام لغة ترميز ملائمة لفهم الشخص. في حالتي ، تم اختيار YAML (لأنه يحل المهمة الإضافية المتمثلة في اختيار تنسيق قابل للقراءة البشرية) . في الواقع ، يعتمد نوع لغة الترميز للملفات التي تحتوي على مجموعات بيانات على المتطلبات المقدمة في المشروع.




تطبيق


نظرًا لأن الدعامة الرئيسية للكون في البرمجة هي الاتفاق ، فسيتعين علينا تقديم العديد منها لحلنا.


اعتراض


بادئ ذي بدء ، يستخدم هذا الحل وظيفة اعتراض pytest_generate_tests ( wiki ) ، والتي تبدأ في مرحلة إنشاء حالات الاختبار ، و metafunc للوسيطة الخاصة metafunc ، والتي تتيح لنا تحديد معلمة الوظيفة. في هذه المرحلة ، يتكرر pytest على كل وظيفة اختبار وتنفيذ شفرة التوليد اللاحقة لها.


الحجج


يجب عليك تحديد قائمة شاملة من المعلمات لوظائف الاختبار. في حالتي ، يكون القاموس test_input وأي نوع بيانات (غالبًا ما يكون سلسلة أو عددًا صحيحًا) في expected_result . نحن بحاجة إلى هذه المعلمات للاستخدام في metafunc.parametrize(...) .


البارامترات


تكرر هذه الوظيفة تمامًا عملية تثبيت المعلمات @pytest.mark.parametrize ، والتي تأخذ كحجة أولى سلسلة تسرد وسيطات دالة الاختبار (في حالتنا "test_input, expected_result" ) وقائمة من البيانات التي ستعمل من خلالها على إنشاء حالات الاختبار الخاصة بنا (على سبيل المثال ، [(1, 2), (2, 4), (3, 6)] ).


في المعركة ، سيبدو كما يلي:


 @pytest.mark.parametrize("test_input, expected_result", [(1, 2), (2, 4), (3, 6)]) def test_multiplication(test_input, expected_result): assert test_input * 2 == expected_result 

وفي حالتنا ، سنشير إلى ذلك مقدمًا:


 #  ... return metafunc.parametrize("test_input, expected", test_cases) #  `[(1, 2), (2, 4), (3, 6)]` 

تصفية


من هنا يتبع أيضًا تخصيص وظائف الاختبار التي تتطلب بيانات من ملف ، وتلك التي تستخدم بيانات ثابتة / ديناميكية. سنطبق هذا التصفية قبل تحليل المعلومات من الملف.


يمكن أن تكون المرشحات نفسها ، على سبيل المثال:


  • وظيفة علامة اسمه yaml :

 #     -  if not hasattr(metafunc.function, 'pytestmark'): return #            mark_names = [ mark.name for mark in metafunc.function.pytestmark ] #   ,        if 'yaml' not in mark_names: return 

خلاف ذلك ، يمكن تنفيذ نفس المرشح مثل هذا:


 #           if Mark(name='yaml', args=(), kwargs={}) not in metafunc.function.pytestmark: return 

  • الوسيطة الدالة test_input :

 #   ,     test_input if 'test_input' not in metafunc.fixturenames: return 

هذا الخيار يناسبني أكثر.




يؤدي


نحتاج إلى إضافة الجزء الذي نحلل فيه البيانات من الملف فقط. لن يكون هذا صعبًا في حالة yaml (وكذلك json ، xml ، وما إلى ذلك) ، لذلك نجمع كل شيء في الكومة.


 # conftest.py import os import yaml import pytest def pytest_generate_tests(metafunc): #   ,     test_input if 'test_input' not in metafunc.fixturenames: return #     dir_path = os.path.dirname(os.path.abspath(metafunc.module.__file__)) #       file_path = os.path.join(dir_path, metafunc.function.__name__ + '.yaml') #    with open(file_path) as f: test_cases = yaml.full_load(f) #       if not test_cases: raise ValueError("Test cases not loaded") return metafunc.parametrize("test_input, expected_result", test_cases) 

نكتب سيناريو اختبار مثل هذا:


 # test_script.py import pytest def test_multiplication(test_input, expected_result): assert test_input * 2 == expected_result 

ملف البيانات:


 # test_multiplication.yaml - !!python/tuple [1,2] - !!python/tuple [1,3] - !!python/tuple [1,5] - !!python/tuple [2,4] - !!python/tuple [3,4] - !!python/tuple [5,4] 

نحصل على قائمة حالات الاختبار التالية:


  pytest /test_script.py --collect-only ======================== test session starts ======================== platform linux -- Python 3.7.4, pytest-5.2.1, py-1.8.0, pluggy-0.13.0 rootdir: /pytest_habr collected 6 items <Module test_script.py> <Function test_multiplication[1-2]> <Function test_multiplication[1-3]> <Function test_multiplication[1-5]> <Function test_multiplication[2-4]> <Function test_multiplication[3-4]> <Function test_multiplication[5-4]> ======================== no tests ran in 0.04s ======================== 

وعن طريق تشغيل البرنامج النصي ، هذه النتيجة: 4 failed, 2 passed, 1 warnings in 0.11s




إضافات. المهام


قد ينهي هذا المقالة ، ولكن من أجل التعقيد ، سأضيف معرفات أكثر ملاءمة إلى وظيفتنا ، مع تحليل بيانات آخر ووضع علامة على كل حالة اختبار فردية.


لذلك ، على الفور ، الكود:


 # conftest.py import os import yaml import pytest def pytest_generate_tests(metafunc): def generate_id(input_data, level): level += 1 #      INDENTS = { # level: (levelmark, addition_indent) 1: ('_', ['', '']), 2: ('-', ['[', ']']) } COMMON_INDENT = ('-', ['[', ']']) levelmark, additional_indent = INDENTS.get(level, COMMON_INDENT) #     -     if level > 3: return additional_indent[0] + type(input_data).__name__ + additional_indent[1] #    elif isinstance(input_data, (str, bool, float, int)): return str(input_data) #   elif isinstance(input_data, (list, set, tuple)): #   ,    ,   list_repr = levelmark.join( [ generate_id(input_value, level=level) \ for input_value in input_data ]) return additional_indent[0] + list_repr + additional_indent[1] #      elif isinstance(input_data, dict): return '{' + levelmark.join(input_data.keys()) + '}' #     else: return None #   ,     test_input if 'test_input' not in metafunc.fixturenames: return #     dir_path = os.path.dirname(os.path.abspath(metafunc.module.__file__)) #       file_path = os.path.join(dir_path, metafunc.function.__name__ + '.yaml') #    with open(file_path) as f: raw_test_cases = yaml.full_load(f) #       if not raw_test_cases: raise ValueError("Test cases not loaded") #    - test_cases = [] #      for case_id, test_case in enumerate(raw_test_cases): #    marks = [ getattr(pytest.mark, name) for name in test_case.get("marks", []) ] #    ,   case_id = test_case.get("id", generate_id(test_case["test_data"], level=0)) #         pytest.param test_cases.append(pytest.param(*test_case["test_data"], marks=marks, id=case_id)) return metafunc.parametrize("test_input, expected_result", test_cases) 

وفقًا لذلك ، نقوم بتغيير شكل ملف YAML الخاص بنا:


 # test_multiplication.yaml - test_data: [1, 2] id: 'one_two' - test_data: [1,3] marks: ['xfail'] - test_data: [1,5] marks: ['skip'] - test_data: [2,4] id: "it's good" marks: ['xfail'] - test_data: [3,4] marks: ['negative'] - test_data: [5,4] marks: ['more_than'] 

ثم سيتغير الوصف إلى:


 <Module test_script.py> <Function test_multiplication[one_two]> <Function test_multiplication[1_3]> <Function test_multiplication[1_5]> <Function test_multiplication[it's good]> <Function test_multiplication[3_4]> <Function test_multiplication[5_4]> 

وسيتم الإطلاق: 2 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 2 warnings in 0.12s


PS: تحذيرات - لأن لا يتم تسجيل علامات مكتوبة ذاتيا في pytest.ini


في تطوير الموضوع


جاهز للمناقشة في التعليقات أسئلة حول النوع:


  • ما هي أفضل طريقة لكتابة ملف yaml؟
  • في أي تنسيق هو أكثر ملاءمة لتخزين بيانات الاختبار؟
  • ما هي حالة الاختبار الإضافية المطلوبة في مرحلة التوليد؟
  • هل أحتاج إلى معرفات لكل حالة؟

Source: https://habr.com/ru/post/ar472766/


All Articles