"""Testing utilities for tests.""" from contextlib import nullcontext from typing import ContextManager, Dict, List, Optional, Tuple import pytest CASE_ID_FORMAT: str = 'case {ord}/{len}: {id}' def parametrize(argnames, cases: List[Tuple]): """ Parametrize test by list of Iterables. It actually fully replicates pytest.mark.parametrize, but it provides a custom test id like "case 1/10: " """ params = [param.strip() for param in argnames.split(',')] dicts = [dict(zip(params, case)) for case in cases] return parametrize_by_dicts(argnames, dicts) def parametrize_by_dicts( argnames, cases: List[Dict]): """ Parametrize test by list of dictionaries. Args: argnames: comma-separated string of parameter names. cases: list of dictionaries with parameters. Each dictionary should have keys corresponding to argnames. If some argument name is missing, it will be set to None. The keys 'case' and 'marks' are reserved. 'case' is used to provide a custom test id. 'marks' is used to provide a list of pytest marks. """ params = [param.strip() for param in argnames.split(',')] if 'marks' in params: raise ValueError('marks is a reserved parameter name') # we let 'case' to be passed to the test function valid_keys = {'case', 'marks'} | set(params) argvalues = [] for i, case in enumerate(cases, start=1): case_id = CASE_ID_FORMAT.format( ord=i, id=case.get('case') or '', len=len(cases) ) extra_keys = set(case.keys()) - valid_keys if extra_keys: raise ValueError(f'Extra keys "{extra_keys}" in case "{case_id}"') param = pytest.param( *[case.get(param) for param in params], id=case_id, marks=case.get('marks', []) ) argvalues.append(param) return pytest.mark.parametrize( argnames, argvalues ) def raises_optionally( expected_exception, match: Optional[str] = None ) -> ContextManager: """Context manager that validates raising of expected_exception. 1. It wraps pytest.raises to check if the expected exception is raised. 2. if no expected exception is provided, it does nothing. 3. optionally validates the exception message for substring. """ if match and not expected_exception: raise ValueError('match makes sense only with expected_exception') if not expected_exception: return nullcontext() return pytest.raises(expected_exception, match=match)