Parametrierung aus Datei in py.test

Im Bereich des automatischen Testens finden Sie verschiedene Tools. Beispielsweise ist py.test eine der beliebtesten Lösungen zum Schreiben von automatischen Tests in Python.


Nachdem ich viele Ressourcen im Zusammenhang mit pytest durchgesehen und die Dokumentation auf der offiziellen Website des Projekts studiert hatte, konnte ich keine direkte Beschreibung der Lösung für eine der Hauptaufgaben finden - das Ausführen von Tests mit Testdaten, die in einer separaten Datei gespeichert sind. Ansonsten kann gesagt werden, dass Parameter aus der Datei (en) in Testfunktionen geladen oder direkt aus der Datei parametrisiert werden. Ein solches Verfahren wird nirgendwo in den Feinheiten beschrieben, und die einzige Erwähnung dieses Merkmals findet sich nur in einer Zeile der Pytest-Dokumentation.


In diesem Artikel werde ich über meine Lösung für dieses Problem sprechen.




Herausforderung


Die Hauptaufgabe besteht darin, aus den entsprechenden test_input Testfälle in Form der Parameter test_input und expected_result in jede einzelne Testfunktion zu generieren.


Zusätzliche Aufgaben:


  • Wählen Sie eine fĂĽr Menschen lesbare Formatierung von Dateien mit Testfällen.
  • Lassen Sie die Fähigkeit, fest codierte Testfälle zu unterstĂĽtzen.
  • Zeigen Sie fĂĽr jeden Fall eindeutige Kennungen an.

Toolkit


In diesem Artikel verwende ich Python 3 (2.7 ist ebenfalls geeignet), Pyyaml ​​und pytest (Versionen 5+ für Python 3 oder 4.6 für Python 2.7), ohne Plugins von Drittanbietern zu verwenden. Zusätzlich wird die Standard- os verwendet.


Die Datei selbst, aus der wir Testfälle entnehmen, muss mit einer Markup-Sprache strukturiert werden, die für eine Person bequem zu verstehen ist. In meinem Fall wurde YAML ausgewählt (weil es die zusätzliche Aufgabe der Auswahl eines für Menschen lesbaren Formats löst) . Welche Art von Auszeichnungssprache für Dateien mit Datensätzen Sie benötigen, hängt von den Anforderungen ab, die an das Projekt gestellt werden.




Implementierung


Da die Hauptsäule des Universums bei der Programmierung die Übereinstimmung ist, müssen wir für unsere Lösung mehrere davon einführen.


Abfangen


Diese Lösung verwendet zunächst die pytest_generate_tests ( wiki ), die in der Phase der Generierung von Testfällen beginnt, und das Argument metafunc , mit dem wir die Funktion parametrisieren können. Zu diesem Zeitpunkt durchläuft pytest jede Testfunktion und führt den nachfolgenden Generierungscode dafür aus.


Argumente


Sie müssen eine vollständige Liste von Parametern für Testfunktionen definieren. In meinem Fall ist das Wörterbuch test_input und jeder Datentyp (meistens eine Zeichenfolge oder eine Ganzzahl) in expected_result . Wir benötigen diese Parameter zur Verwendung in metafunc.parametrize(...) .


Parametrierung


Diese Funktion wiederholt die Operation der Parametrisierungsvorrichtung @pytest.mark.parametrize vollständig. Als erstes Argument wird eine Zeichenfolge verwendet, in der die Argumente der Testfunktion (in unserem Fall "test_input, expected_result" ) und eine Liste von Daten aufgeführt sind, mit denen die Testfälle erstellt werden (z. B. [(1, 2), (2, 4), (3, 6)] ).


Im Kampf wird es so aussehen:


 @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 

Und in unserem Fall werden wir dies im Voraus angeben:


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

Filtern


Ab hier folgt auch die Zuordnung der Testfunktionen, fĂĽr die Daten aus einer Datei erforderlich sind, von denen, die statische / dynamische Daten verwenden. Wir werden diese Filterung anwenden, bevor wir die Informationen aus der Datei analysieren.


Die Filter selbst können beliebig sein, zum Beispiel:


  • Funktionsmarker namens 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 

Andernfalls kann derselbe Filter wie folgt implementiert werden:


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

  • Das Argument fĂĽr die Funktion test_input :

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

Diese Option hat mir am besten gefallen.




Ergebnis


Wir mĂĽssen nur den Teil hinzufĂĽgen, in dem wir die Daten aus der Datei analysieren. Dies wird im Fall von Yaml (sowie von JSON , XML usw.) nicht schwierig sein, daher sammeln wir alles auf dem Haufen.


 # 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) 

Wir schreiben ein Testskript wie folgt:


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

Eine Datendatei:


 # 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] 

Wir erhalten folgende Liste von Testfällen:


  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 ======================== 

Und durch AusfĂĽhren des Skripts ergibt sich folgendes Ergebnis: 4 failed, 2 passed, 1 warnings in 0.11s




HinzufĂĽgen. Aufgaben


Dies könnte das Ende des Artikels sein, aber für die größere Komplexität werde ich unserer Funktion bequemere Bezeichner hinzufügen, eine weitere Datenanalyse und Markierung für jeden einzelnen Testfall.


Also sofort der Code:


 # 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) 

Dementsprechend ändern wir, wie unsere YAML-Datei aussehen wird:


 # 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'] 

Dann ändert sich die Beschreibung zu:


 <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]> 

Und der Start wird sein: 2 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 2 warnings in 0.12s


PS: Warnungen - weil Selbstgeschriebene Marker werden nicht in pytest.ini aufgezeichnet


In der Entwicklung des Themas


Bereit, in den Kommentaren Fragen zum Typ zu diskutieren:


  • Was ist der beste Weg, um eine Yaml-Datei zu schreiben?
  • In welchem ​​Format ist es bequemer, Testdaten zu speichern?
  • Welcher zusätzliche Testfall wird in der Generierungsphase benötigt?
  • Benötige ich fĂĽr jeden Fall Kennungen?

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


All Articles