Paramétrage à partir d'un fichier dans py.test

Dans le domaine des tests automatiques, vous pouvez trouver divers outils, par exemple, py.test est l'une des solutions les plus populaires pour écrire des auto-tests en Python.


Ayant parcouru de nombreuses ressources liées à pytest et ayant étudié la documentation du site officiel du projet, je n'ai pas pu trouver de description directe de la solution pour l'une des tâches principales - exécuter des tests avec des données de test stockées dans un fichier séparé. Sinon, on peut dire, le chargement des paramètres dans les fonctions de test depuis le (s) fichier (s) ou le paramétrage directement depuis le fichier. Une telle procédure n'est décrite nulle part dans les subtilités et la seule mention de cette fonctionnalité se trouve dans une seule ligne de la documentation Pytest.


Dans cet article, je vais parler de ma solution à ce problème.




Défi


La tâche principale consiste à générer des cas de test sous la forme des paramètres test_input et test_input dans chaque fonction de test individuelle à partir des noms de fonction de fichier correspondants.


Tâches supplémentaires:


  • choisissez une mise en forme lisible par l'homme des fichiers avec des cas de test;
  • laisser la possibilité de prendre en charge des cas de test codés en dur;
  • afficher des identifiants clairs pour chaque cas.

Boîte à outils


Dans l'article, j'utilise Python 3 (2.7 convient également), pyyaml ​​et pytest (versions 5+ pour Python 3, ou 4.6 pour Python 2.7) sans utiliser de plugins tiers. De plus, la bibliothèque os standard sera utilisée.


Le fichier lui-même à partir duquel nous prendrons des cas de test doit être structuré à l'aide d'un langage de balisage pratique pour une personne. Dans mon cas, YAML a été choisi (car il résout la tâche supplémentaire de choisir un format lisible par l'homme) . En fait, le type de langage de balisage pour les fichiers avec les ensembles de données dont vous avez besoin dépend des exigences présentées dans le projet.




Implémentation


Puisque le principal pilier de l'univers en programmation est l'accord, nous devrons en introduire plusieurs pour notre solution.


Interception


Pour commencer, cette solution utilise la fonction d'interception pytest_generate_tests ( wiki ), qui commence au stade de la génération des cas de test, et son argument metafunc , qui nous permet de paramétrer la fonction. À ce stade, pytest parcourt chaque fonction de test et exécute le code de génération suivant pour celle-ci.


Arguments


Vous devez définir une liste exhaustive de paramètres pour les fonctions de test. Dans mon cas, le dictionnaire est test_input et tout type de données (le plus souvent une chaîne ou un entier) dans expected_result . Nous avons besoin de ces paramètres pour les utiliser dans metafunc.parametrize(...) .


Paramétrisation


Cette fonction répète complètement le fonctionnement du @pytest.mark.parametrize paramétrage @pytest.mark.parametrize , qui prend comme premier argument une chaîne répertoriant les arguments de la fonction de test (dans notre cas "test_input, expected_result" ) et une liste de données par lesquelles il va itérer pour créer nos cas de test (par exemple, [(1, 2), (2, 4), (3, 6)] ).


Au combat, cela ressemblera à ceci:


 @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 

Et dans notre cas, nous l'indiquerons à l'avance:


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

Filtrage


D'ici suit également l'allocation de ces fonctions de test où les données d'un fichier sont nécessaires, de celles qui utilisent des données statiques / dynamiques. Nous appliquerons ce filtrage avant d'analyser les informations du fichier.


Les filtres eux-mêmes peuvent être quelconques, par exemple:


  • Marqueur de fonction nommé 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 

Sinon, le même filtre peut être implémenté comme ceci:


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

  • L'argument de la fonction test_input :

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

Cette option me convenait le mieux.




Résultat


Nous devons ajouter uniquement la partie où nous analysons les données du fichier. Ce ne sera pas difficile dans le cas de yaml (ainsi que json, xml, etc.) , nous collectons donc tout sur le tas.


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

Nous écrivons un script de test comme celui-ci:


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

Un fichier de données:


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

Nous obtenons la liste suivante de cas de test:


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

Et en exécutant le script, ce résultat: 4 failed, 2 passed, 1 warnings in 0.11s




Ajouter. affectations


Cela pourrait terminer l'article, mais pour des raisons de complexité, j'ajouterai des identifiants plus pratiques à notre fonction, une autre analyse des données et un marquage de chaque cas de test individuel.


Alors, tout de suite, le 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) 

En conséquence, nous modifions l'apparence de notre fichier 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'] 

Ensuite, la description changera en:


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

Et le lancement sera: 2 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 2 warnings in 0.12s


PS: avertissements - parce que les marqueurs auto-écrits ne sont pas enregistrés dans pytest.ini


En développement du sujet


Prêt à discuter dans les commentaires des questions sur le type:


  • quelle est la meilleure façon d'écrire un fichier yaml?
  • Dans quel format est-il plus pratique de stocker les données de test?
  • Quel cas de test supplémentaire est nécessaire au stade de la génération?
  • Ai-je besoin d'identifiants pour chaque cas?

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


All Articles