Parametrización desde archivo en py.test

En el campo de las pruebas automáticas, puede encontrar diferentes herramientas, por ejemplo, py.test es una de las soluciones más populares para escribir pruebas automáticas en Python.


Después de haber revisado muchos recursos relacionados con pytest y haber estudiado la documentación del sitio web oficial del proyecto, no pude encontrar una descripción directa de la solución para una de las tareas principales: ejecutar pruebas con datos de prueba almacenados en un archivo separado. De lo contrario, se puede decir, la carga de parámetros en funciones de prueba desde el archivo (s) o la parametrización desde el archivo directamente. Tal procedimiento no se describe en ninguna parte de las complejidades y la única mención de esta característica está en solo una línea de la documentación de pytest.


En este artículo hablaré sobre mi solución a este problema.




Desafío


La tarea principal es generar casos de prueba en forma de los parámetros test_input y test_input en cada función de prueba individual a partir de los nombres de función de archivo correspondientes.


Tareas adicionales:


  • elija un formato de archivos legible para humanos con casos de prueba;
  • dejar la capacidad de soportar casos de prueba codificados;
  • mostrar identificadores claros para cada caso.

Kit de herramientas


En el artículo, uso Python 3 (2.7 también es adecuado), pyyaml ​​y pytest (versiones 5+ para Python 3 o 4.6 para Python 2.7) sin usar complementos de terceros. Además, se utilizará la biblioteca os estándar.


El archivo mismo del que tomaremos casos de prueba debe estructurarse utilizando un lenguaje de marcado que sea conveniente para que una persona lo entienda. En mi caso, se eligió YAML (porque resuelve la tarea adicional de elegir un formato legible para humanos) . De hecho, qué tipo de lenguaje de marcado para archivos con conjuntos de datos necesita depende de los requisitos presentados en el proyecto.




Implementación


Dado que el pilar principal del universo en la programación es el acuerdo, tendremos que introducir varios de ellos para nuestra solución.


Intercepcion


Para empezar, esta solución utiliza la función de intercepción pytest_generate_tests ( wiki ), que se inicia en la etapa de generación de casos de prueba, y su argumento metafunc , que nos permite parametrizar la función. En este punto, pytest itera sobre cada función de prueba y ejecuta el código de generación posterior para ella.


Argumentos


Debe definir una lista exhaustiva de parámetros para las funciones de prueba. En mi caso, el diccionario es test_input y cualquier tipo de datos (más a menudo una cadena o un entero) en el resultado expected_result . Necesitamos estos parámetros para usar en metafunc.parametrize(...) .


Parametrización


Esta función repite completamente la operación del @pytest.mark.parametrize parametrización @pytest.mark.parametrize , que toma como primer argumento una cadena que enumera los argumentos de la función de prueba (en nuestro caso "test_input, expected_result" ) y una lista de datos mediante los cuales iterará para crear nuestros casos de prueba (por ejemplo, [(1, 2), (2, 4), (3, 6)] ).


En la batalla, se verá así:


 @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 

Y en nuestro caso, indicaremos esto de antemano:


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

Filtrado


A partir de aquí también se sigue la asignación de esas funciones de prueba donde se requieren datos de un archivo, de aquellos que usan datos estáticos / dinámicos. Aplicaremos este filtrado antes de analizar la información del archivo.


Los filtros en sí pueden ser cualquiera, por ejemplo:


  • Marcador de función llamado 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 

De lo contrario, el mismo filtro puede implementarse así:


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

  • El argumento de la función test_input :

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

Esta opción me convenía más.




Resultado


Necesitamos agregar solo la parte donde analizamos los datos del archivo. Esto no será difícil en el caso de yaml (así como json, xml, etc.) , por lo que recopilamos todo en el montón.


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

Escribimos un script de prueba como este:


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

Un archivo de datos:


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

Obtenemos la siguiente lista de casos de prueba:


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

Y al ejecutar el script, este resultado: 4 failed, 2 passed, 1 warnings in 0.11s




Añadir asignaciones


Esto podría terminar el artículo, pero en aras de la complejidad, agregaré identificadores más convenientes a nuestra función, otro análisis y marcado de datos de cada caso de prueba individual.


Entonces, de inmediato, el código:


 # 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 consecuencia, cambiamos la apariencia de nuestro archivo 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'] 

Entonces la descripción cambiará a:


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

Y el lanzamiento será: 2 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 2 warnings in 0.12s


PD: advertencias, porque los marcadores autoescritos no se registran en pytest.ini


En desarrollo del tema


Listo para discutir en los comentarios preguntas sobre el tipo:


  • ¿Cuál es la mejor manera de escribir un archivo yaml?
  • ¿En qué formato es más conveniente almacenar datos de prueba?
  • ¿Qué caso de prueba adicional se necesita en la etapa de generación?
  • ¿Necesito identificadores para cada caso?

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


All Articles