简体   繁体   English

如何使用 pytest 动态参数化生成的测试 lambda 函数

[英]How to dynamically parametrize generated test lambda functions with pytest

Disclaimer: Yes I am well aware this is a mad attempt.免责声明:是的,我很清楚这是一次疯狂的尝试。

Use case:用例:

I am reading from a config file to run a test collection where each such collection comprises of set of test cases with corresponding results and a fixed setup.我正在从配置文件中读取以运行测试集合,其中每个此类集合都包含一组具有相应结果和固定设置的测试用例。

Flow (for each test case):流程(对于每个测试用例):

  1. Setup: wipe and setup database with specific test case dataset (glorified SQL file)设置:使用特定的测试用例数据集擦除和设置数据库(美化 SQL 文件)
  2. load expected test case results from csv从 csv 加载预期的测试用例结果
  3. execute collections query/report执行 collections 查询/报告
  4. compare results.比较结果。

Sounds good, except the people writing the test cases are more from a tech admin perspective, so the goal is to enable this without writing any python code.听起来不错,除了编写测试用例的人更多是从技术管理员的角度来看,所以目标是在不编写任何 python 代码的情况下启用此功能。

code代码

Assume these functions exist.假设这些函数存在。

# test_queries.py
def gather_collections(): (collection, query, config)
def gather_cases(collection): (test_case)
def load_collection_stubs(collection): None
def load_case_dataset(test_case): None
def read_case_result_csv(test_case): [csv_result]
def execute(query): [query_result]


class TestQueries(unittest.TestCase):
    def setup_method(self, method):
        collection = self._item.name.replace('test_', '')
        load_collection_stubs(collection)
# conftest.py
import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item, nextitem):
    item.cls._item = item
    yield

Example Data示例数据

Collection stubs / data (setting up of environment)收集存根/数据(环境设置)

-- stubs/test_setup_log.sql
DROP DATABASE IF EXISTS `test`;
CREATE DATABASE `test`;
USE test;
CREATE TABLE log (`id` int(9) NOT NULL AUTO_INCREMENT, `timestamp` datetime NOT NULL DEFAULT NOW(), `username` varchar(100) NOT NULL, `message` varchar(500));

Query to test查询要测试

-- queries/count.sql
SELECT count(*) as `log_count` from test.log where username = 'unicorn';

Test case 1 input data测试用例 1 输入数据

-- test_case_1.sql
INSERT INTO log (`id`, `timestamp`, `username`, `message`)
VALUES
    (1,'2020-12-18T11:23.01Z', 'unicorn', 'user logged in'),
    (2,'2020-12-18T11:23.02Z', 'halsey', 'user logged off'),
    (3,'2020-12-18T11:23.04Z', 'unicorn', 'user navigated to home')

Test case 1 expected result test_case_1.csv测试用例 1 预期结果test_case_1.csv

log_count
2

Attempt 1尝试 1

for collection, query, config in gather_collections():
    test_method_name = 'test_{}'.format(collection)

    LOGGER.debug("collections.{}.test - {}".format(collection, config))
    cases = gather_cases(collection)
    LOGGER.debug("collections.{}.cases - {}".format(collection, cases))
    setattr(
        TestQueries,
        test_method_name,
        pytest.mark.parametrize(
            'case_name',
            cases,
            ids=cases
        )(
            lambda self, case_name: (
                load_case_dataset(case_name),
                self.assertEqual(execute(query, case_name), read_case_result_csv( case_name))
            )
        )
    )

Attempt 2尝试 2

for collection, query, config in gather_collections():
    test_method_name = 'test_{}'.format(collection)

    LOGGER.debug("collections.{}.test - {}".format(collection, config))
    setattr(
        TestQueries,
        test_method_name,
        lambda self, case_name: (
            load_case_dataset(case_name),
            self.assertEqual(execute(query, case_name), read_case_result_csv(case_name))
        )
    )

def pytest_generate_tests(metafunc):
    collection = metafunc.function.__name__.replace('test_', '')
    # FIXME logs and id setting not working
    cases = gather_cases(collection)
    LOGGER.info("collections.{}.pytest.cases - {}".format(collection, cases))

    metafunc.parametrize(
        'case_name',
        cases,
        ids=cases
    )

So I figured it out, but it's not the most elegant solution.所以我想通了,但这不是最优雅的解决方案。 Essentially you use one function and then use some of pytests hooks to change the function names for reporting.本质上,您使用一个 function,然后使用一些 pytests 挂钩来更改 function 名称以进行报告。

There are numerous issues, eg if you don't use pytest.param to pass the parameters to parametrize then you do not have the required information available.有很多问题,例如,如果您不使用pytest.param将参数传递给parametrize ,那么您没有可用的所需信息。 Also the method passed to setup_method is not aware of the actual iteration being run when its called, so I had to hack that in with the iter counter.此外,传递给setup_methodmethod在调用它时并不知道实际迭代正在运行,所以我不得不用iter计数器来破解它。

# test_queries.py
def gather_tests():
    global TESTS

    for test_collection_name in TESTS.keys():
        LOGGER.debug("collections.{}.gather - {}".format(test_collection_name, TESTS[test_collection_name]))
        query = path.join(SRC_DIR, TESTS[test_collection_name]['query'])
        cases_dir = TESTS[test_collection_name]['cases']
        result_sets = path.join(TEST_DIR, cases_dir, '*.csv')

        for case_result_csv in glob.glob(result_sets):
            test_case_name = path.splitext(path.basename(case_result_csv))[0]
            yield test_case_name, query, test_collection_name, TESTS[test_collection_name]



class TestQueries():
    iter = 0

    def setup_method(self, method):
        method_name = method.__name__  # or self._item.originalname
        global TESTS

        if method_name == 'test_scripts_reports':
            _mark = next((m for m in method.pytestmark if m.name == 'parametrize' and 'collection_name' in m.args[0]), None)
            if not _mark:
                raise Exception('test {} missing collection_name parametrization'.format(method_name))  # nothing to do here

            _args = _mark.args[0]
            _params = _mark.args[1]
            LOGGER.debug('setup_method: _params - {}'.format(_params))
            if not _params:
                raise Exception('test {} missing pytest.params'.format(method_name))  # nothing to do here

            _currparams =_params[self.iter]
            self.iter += 1

            _argpos = [arg.strip() for arg in _args.split(',')].index('collection_name')
            collection = _currparams.values[_argpos]
            LOGGER.debug('collections.{}.setup_method[{}] - {}'.format(collection, self.iter, _currparams))
            load_collection_stubs(collection)


    @pytest.mark.parametrize(
        'case_name, collection_query, collection_name, collection_config',
        [pytest.param(*c, id='{}:{}'.format(c[2], c[0])) for c in gather_tests()]
    )
    def test_scripts_reports(self, case_name, collection_query, collection_name, collection_config):
        if not path.isfile(collection_query):
            pytest.skip("report query does not exist: {}".format(collection_query))

        LOGGER.debug("test_scripts_reports.{}.{} - ".format(collection_name, case_name))
        load_case_dataset( case_name)
        assert execute(collection_query, case_name) == read_case_result_csv(case_name)

Then to make the test ids more human you can do this然后为了使测试 id 更人性化,你可以这样做

# conftest.py
def pytest_collection_modifyitems(items):
    # https://stackoverflow.com/questions/61317809/pytest-dynamically-generating-test-name-during-runtime
    for item in items:
        if item.originalname == 'test_scripts_reports':
            item._nodeid = re.sub(r'::\w+::\w+\[', '[', item.nodeid)

the result with the following files:结果包含以下文件:

stubs/
  00-wipe-db.sql
  setup-db.sql
queries/
  report1.sql
collection/
  report1/
    case1.sql
    case1.csv
    case2.sql
    case2.csv

# results (with setup_method firing before each test and loading the appropriate stubs as per configuration)
FAILED test_queries.py[report1:case1]
FAILED test_queries.py[report1:case2]

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM