从py.test中的文件进行参数化

在自动测试领域,您可以找到不同的工具,例如py.test是使用Python编写自动测试的最受欢迎的解决方案之一。


在浏览了许多与pytest相关的资源并研究了该项目官方网站上的文档后,我找不到对主要任务之一的解决方案的直接描述-使用存储在单独文件中的测试数据运行测试。 否则,可以说是从文件将参数加载到测试功能中,或者直接从文件中加载参数。 此类程序在复杂性中没有任何地方描述,并且仅在pytest文档的一行中提到了此功能。


在本文中,我将讨论该问题的解决方案。




挑战赛


主要任务是从相应的文件函数名称将test_inputtest_input参数形式的测试用例生成到每个单独的测试函数中。


其他任务:


  • 选择人类可读的带有测试用例的文件格式;
  • 保留支持硬编码测试用例的能力;
  • 显示每种情况的明确标识符。

工具包


在本文中,我使用了Python 3(也适用于2.7),pyyaml和pytest (Python 3的版本为5 +,Python 2.7的版本为4.6),而没有使用第三方插件。 另外,将使用标准的os库。


我们将使用测试用例的文件本身需要使用便于人类理解的标记语言来构造。 在我的情况下,选择了YAML (因为它解决了选择人类可读格式的额外任务) 。 实际上,您需要哪种具有数据集的文件标记语言取决于项目中提出的要求。




实作


由于编程领域的主要支柱是协议,因此我们必须为解决方案引入一些协议。


拦截


首先,此解决方案使用pytest_generate_testswiki )拦截函数(该函数从生成测试用例的阶段开始)及其参数metafunc ,该参数允许我们对该函数进行参数化。 此时,pytest遍历每个测试函数并为其执行后续的生成代码。


争论


您必须为测试功能定义详尽的参数列表。 在我的情况下,字典是test_input而任何数据类型(通常是字符串或整数)都test_input中。 我们需要在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/zh-CN472766/


All Articles