diff --git a/AUTHORS b/AUTHORS index 163b317bc42..1dd5839ac53 100644 --- a/AUTHORS +++ b/AUTHORS @@ -267,6 +267,7 @@ Vidar T. Fauske Virgil Dupras Vitaly Lashmanov Vlad Dragos +Vladyslav Rachek Volodymyr Piskun Wei Lin Wil Cooley diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 7230f2b00db..3a95e819f06 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -398,6 +398,9 @@ The result of this test will be successful: .. regendoc:wipe +Note, that each argument in `parametrize` list should be explicitly declared in corresponding +python test function or via `indirect`. + Parametrizing test methods through per-class configuration -------------------------------------------------------------- diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1b01f4faaef..e93c5d2baa0 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -998,6 +998,7 @@ def parametrize( scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) self._validate_if_using_arg_names(argnames, indirect) + self._validate_explicit_parameters(argnames, indirect) arg_values_types = self._resolve_arg_value_types(argnames, indirect) @@ -1151,6 +1152,34 @@ def _validate_if_using_arg_names(self, argnames, indirect): pytrace=False, ) + def _validate_explicit_parameters(self, argnames, indirect): + """ + The argnames in *parametrize* should either be declared explicitly via + indirect list or explicitly in the function + + :param List[str] argnames: list of argument names passed to ``parametrize()``. + :param indirect: same ``indirect`` parameter of ``parametrize()``. + :raise ValueError: if validation fails + """ + func_name = self.function.__name__ + if type(indirect) is bool and indirect is True: + return + parametrized_argnames = list() + funcargnames = _pytest.compat.getfuncargnames(self.function) + if type(indirect) is list: + for arg in argnames: + if arg not in indirect: + parametrized_argnames.append(arg) + elif indirect is False: + parametrized_argnames = argnames + for arg in parametrized_argnames: + if arg not in funcargnames: + raise ValueError( + f'In function "{func_name}":\n' + f'Parameter "{arg}" should be declared explicitly via indirect\n' + f"or in function itself" + ) + def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): """Find the most appropriate scope for a parametrized call based on its arguments. diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 9b6471cdc50..42f01cbcf84 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1877,3 +1877,53 @@ def test_converted_to_str(a, b): "*= 6 passed in *", ] ) + + def test_parametrize_explicit_parameters_func(self, testdir): + testdir.makepyfile( + """ + import pytest + + + @pytest.fixture + def fixture(arg): + return arg + + @pytest.mark.parametrize("arg", ["baz"]) + def test_without_arg(fixture): + assert "baz" == fixture + """ + ) + result = testdir.runpytest() + result.assert_outcomes(error=1) + result.stdout.fnmatch_lines( + [ + '*In function "test_without_arg"*', + '*Parameter "arg" should be declared explicitly via indirect*', + "*or in function itself*", + ] + ) + + def test_parametrize_explicit_parameters_method(self, testdir): + testdir.makepyfile( + """ + import pytest + + class Test: + @pytest.fixture + def test_fixture(self, argument): + return argument + + @pytest.mark.parametrize("argument", ["foobar"]) + def test_without_argument(self, test_fixture): + assert "foobar" == test_fixture + """ + ) + result = testdir.runpytest() + result.assert_outcomes(error=1) + result.stdout.fnmatch_lines( + [ + '*In function "test_without_argument"*', + '*Parameter "argument" should be declared explicitly via indirect*', + "*or in function itself*", + ] + )