Parametrização do arquivo em py.test

No campo de teste automático, você pode encontrar ferramentas diferentes, por exemplo, py.test é uma das soluções mais populares para escrever testes automáticos em Python.


Depois de passar por muitos recursos relacionados ao pytest e ter estudado a documentação no site oficial do projeto, não consegui encontrar uma descrição direta da solução para uma das principais tarefas - executar testes com dados de teste armazenados em um arquivo separado. Caso contrário, pode-se dizer, o carregamento de parâmetros nas funções de teste do (s) arquivo (s) ou a parametrização do arquivo diretamente. Esse procedimento não é descrito em nenhum lugar nos meandros e a única menção desse recurso está em apenas uma linha da documentação do pytest.


Neste artigo, falarei sobre minha solução para esse problema.




Desafio


A tarefa principal é gerar casos de teste na forma dos parâmetros test_input em cada função de teste individual a partir dos nomes de funções de arquivo correspondentes.


Tarefas adicionais:


  • escolha formatação legível por humanos de arquivos com casos de teste;
  • deixe a capacidade de suportar casos de teste codificados;
  • exibir identificadores claros para cada caso.

Toolkit


No artigo, eu uso o Python 3 (2.7 também é adequado), pyyaml ​​e pytest (versões 5+ para Python 3 ou 4.6 para Python 2.7) sem usar plug-ins de terceiros. Além disso, a biblioteca padrão do sistema os será usada.


O próprio arquivo a partir do qual realizaremos os casos de teste precisa ser estruturado usando uma linguagem de marcação que seja conveniente para uma pessoa entender. No meu caso, o YAML foi escolhido (porque resolve a tarefa adicional de escolher um formato legível por humanos) . De fato, que tipo de linguagem de marcação para arquivos com conjuntos de dados que você precisa depende dos requisitos apresentados no projeto.




Implementação


Como o principal pilar do universo em programação é o acordo, teremos que introduzir vários deles para nossa solução.


Interceptação


Para começar, esta solução usa a função de interceptação pytest_generate_tests ( wiki ), que começa no estágio de geração de casos de teste, e seu argumento metafunc , que nos permite parametrizar a função. Neste ponto, o pytest itera sobre cada função de teste e executa o código de geração subsequente para ela.


Argumentos


Você deve definir uma lista exaustiva de parâmetros para funções de teste. No meu caso, o dicionário é test_input e qualquer tipo de dado (na maioria das vezes uma string ou um número inteiro) no resultado expected_result . Precisamos desses parâmetros para uso em metafunc.parametrize(...) .


Parametrização


Essa função repete completamente a operação do parâmetro @pytest.mark.parametrize , que usa como primeiro argumento uma string que lista os argumentos da função de teste (no nosso caso "test_input, expected_result" ) e uma lista de dados pelos quais iterará para criar nossos casos de teste (por exemplo, [(1, 2), (2, 4), (3, 6)] ).


Na batalha, ficará assim:


 @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 

E no nosso caso, indicaremos isso com antecedência:


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

Filtragem


A partir daqui também segue a alocação dessas funções de teste onde os dados de um arquivo são necessários, daqueles que usam dados estáticos / dinâmicos. Aplicaremos essa filtragem antes de analisar as informações do arquivo.


Os próprios filtros podem ser quaisquer, por exemplo:


  • Marcador de função chamado 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 

Caso contrário, o mesmo filtro pode ser implementado assim:


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

  • O argumento para a função test_input :

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

Esta opção me convinha mais.




Resultado


Precisamos adicionar apenas a parte em que analisamos os dados do arquivo. Isso não será difícil no caso do yaml (assim como no json, xml etc.) , portanto, coletamos tudo no heap.


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

Nós escrevemos um script de teste como este:


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

Um arquivo de dados:


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

Temos a seguinte lista de casos de teste:


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

E executando o script, este resultado: 4 failed, 2 passed, 1 warnings in 0.11s




Adicionar. atribuições


Isso pode terminar o artigo, mas, por uma questão de complexidade, adicionarei identificadores mais convenientes à nossa função, outra análise de dados e marcação de cada caso de teste individual.


Então, imediatamente, o 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) 

Assim, alteramos a aparência do nosso arquivo 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'] 

A descrição mudará para:


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

E o lançamento será: 2 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 2 warnings in 0.12s


PS: avisos - porque marcadores auto-escritos não são registrados no pytest.ini


No desenvolvimento do tópico


Pronto para discutir nos comentários perguntas sobre o tipo:


  • qual é a melhor maneira de escrever um arquivo yaml?
  • Em que formato é mais conveniente armazenar dados de teste?
  • Que caso de teste adicional é necessário no estágio de geração?
  • Preciso de identificadores para cada caso?

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


All Articles