From e88e1eb2b33b9a1041c285ba7240f8f610c258b0 Mon Sep 17 00:00:00 2001 From: Jeremy Hicks Date: Thu, 17 Sep 2020 15:01:38 +0100 Subject: [PATCH 1/6] Fix issue: https://github.com/pytest-dev/pytest-bdd/issues/390 Changed validation of a scenario against its examples table so that the list of parameters defined for the scenario does not have to be the same as the list of parameters defined in the examples table, but can be a subset. This allows columns to be specified in the examples table that are not used in the scenario, but are there for future use or purely for documentation purposes. --- pytest_bdd/parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 8cbfbb145..40e436555 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -265,10 +265,10 @@ def validate(self): """ params = frozenset(sum((list(step.params) for step in self.steps), [])) example_params = set(self.examples.example_params + self.feature.examples.example_params) - if params and example_params and params != example_params: + if params and example_params and params.issubset(example_params): raise exceptions.ScenarioExamplesNotValidError( - """Scenario "{}" in the feature "{}" has not valid examples. """ - """Set of step parameters {} should match set of example values {}.""".format( + """Scenario "{}" in the feature "{}" does not have valid examples. """ + """Set of step parameters {} should be a subset of example values {}.""".format( self.name, self.feature.filename, sorted(params), sorted(example_params) ) ) From 85237902a79ddd621986c5f4d2fdc180a807731a Mon Sep 17 00:00:00 2001 From: Jeremy Hicks Date: Sat, 19 Sep 2020 11:44:51 +0100 Subject: [PATCH 2/6] Update unit tests --- tests/feature/test_outline.py | 56 ++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 3628db8d7..69e50e148 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -81,22 +81,22 @@ def test_outline(request): # fmt: on -def test_wrongly_outlined(testdir): - """Test parametrized scenario when the test function lacks parameters.""" +def test_outline_has_subset_of_parameters(testdir): + """Test parametrized scenario when the test function has a subset of the parameters of the examples.""" testdir.makefile( ".feature", outline=textwrap.dedent( """\ Feature: Outline - Scenario Outline: Outlined with wrong examples + Scenario Outline: Outlined with subset of examples Given there are cucumbers When I eat cucumbers Then I should have cucumbers Examples: - | start | eat | left | unknown_param | - | 12 | 5 | 7 | value | + | start | eat | left | notes | + | 12 | 5 | 7 | Should be ignored | """ ), @@ -108,18 +108,60 @@ def test_wrongly_outlined(testdir): """\ from pytest_bdd import scenario + @scenario("outline.feature", "Outlined with subset of examples", + example_converters=dict(start=int, eat=float, left=str)) + def test_outline(request): + pass + """ + ) + ) + result = testdir.runpytest() + assert_outcomes(result, passed=1) + + +def test_wrongly_outlined_parameters_not_a_subset_of_examples(testdir): + """Test parametrized scenario when the test function has a parameter set which is not a subset of those in the examples table.""" + + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers in my bucket + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + + """ + ), + ) + testdir.makeconftest(textwrap.dedent(STEPS)) + + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario, then + @scenario("outline.feature", "Outlined with wrong examples") def test_outline(request): pass + + @then(parsers.parse('I should have cucumbers in my bucket')) + def stepdef(left, right): + pass """ ) ) result = testdir.runpytest() assert_outcomes(result, errors=1) result.stdout.fnmatch_lines( - '*ScenarioExamplesNotValidError: Scenario "Outlined with wrong examples"*has not valid examples*', + '*ScenarioExamplesNotValidError: Scenario "Outlined with wrong examples"*does not have valid examples*', ) - result.stdout.fnmatch_lines("*should match set of example values [[]'eat', 'left', 'start', 'unknown_param'[]].*") + result.stdout.fnmatch_lines("*should be a subset of example values [[]'eat', 'left', 'start'[]].*") def test_wrong_vertical_examples_scenario(testdir): From 300c174a193d473b4c6ee570a64143e374727e2e Mon Sep 17 00:00:00 2001 From: Kostiantyn Goloveshko Date: Tue, 10 Aug 2021 17:39:13 +0300 Subject: [PATCH 3/6] Allow explicit free variables control at Examples and steps --- CHANGES.rst | 5 +++ README.rst | 40 +++++++++++++++++ pytest_bdd/parser.py | 54 ++++++++++++++++++++--- pytest_bdd/scenario.py | 27 ++++++++++-- tests/feature/test_outline.py | 63 +++++++++++++++++++++++--- tests/feature/test_parametrized.py | 71 ++++++++++++++++++++++++++++++ 6 files changed, 246 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2afab31d5..bf75e0be6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ Changelog ========= +Unreleased +----------- +- Add options to control free variables in Examples and step definitions + + 5.0.0 ----- This release introduces breaking changes, please refer to the :ref:`Migration from 4.x.x`. diff --git a/README.rst b/README.rst index 7bf302d5e..366f6327f 100644 --- a/README.rst +++ b/README.rst @@ -696,6 +696,46 @@ With a parametrized.feature file: The significant downside of this approach is inability to see the test table from the feature file. +It's possible to disallow steps free parameters substitution from fixtures (so test case will fail): + +.. code-block:: python + @pytest.mark.parametrize( + ["start", "eat", "left"], + [(12, 5, 7)], + ) + @scenario( + "parametrized.feature", + "Parametrized given, when, thens", + allow_step_free_variables=False, + ) + def test_parametrized(start, eat, left): + """We don't need to do anything here, everything will be managed by the scenario decorator.""" + +Sometimes you want leave a column not used in steps for specific reason in examples section: + +.. code-block:: gherkin + Feature: Scenario outlines + Scenario Outline: Outlined given, when, thens + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: + | start | eat | left | comment | + | 12 | 5 | 7 | sweet cucumbers!| + + +.. code-block:: python + from pytest_bdd import given, when, then, scenario + + + @scenario( + "outline.feature", + "Outlined given, when, thens", + allow_example_free_variables=True, + ) + def test_outlined(): + pass Organizing your scenarios diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 40e436555..523b5cb11 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -232,6 +232,9 @@ def __init__(self, feature: Feature, name: str, line_number: int, tags=None): self.line_number = line_number self.tags = tags or set() + self.allow_example_free_variables = None + self.allow_step_free_variables = None + def add_step(self, step): """Add step to the scenario. @@ -258,18 +261,57 @@ def render(self, context: typing.Mapping[str, typing.Any]) -> "Scenario": ] return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags) + @property + def params(self): + return frozenset(sum((list(step.params) for step in self.steps), [])) + + def get_example_params(self): + return set(self.examples.example_params + self.feature.examples.example_params) + def validate(self): """Validate the scenario. :raises ScenarioValidationError: when scenario is not valid """ - params = frozenset(sum((list(step.params) for step in self.steps), [])) - example_params = set(self.examples.example_params + self.feature.examples.example_params) - if params and example_params and params.issubset(example_params): + if self.params or self.get_example_params(): + self._validate_example_free_variables() + self._validate_step_free_variables() + + def _validate_example_free_variables(self): + params = self.params + example_params = self.get_example_params() + if self.allow_example_free_variables or example_params.issubset(params): + return + else: + raise exceptions.ScenarioExamplesNotValidError( + ( + """Scenario "{}" in the feature "{}" does not have valid examples. """ + """Set of example parameters {} should be a subset of step """ + """parameters {} if examples free variables are not allowed""" + ).format( + self.name, + self.feature.filename, + sorted(example_params), + sorted(params), + ) + ) + + def _validate_step_free_variables(self): + params = self.params + example_params = self.get_example_params() + if self.allow_step_free_variables or params.issubset(example_params): + return + else: raise exceptions.ScenarioExamplesNotValidError( - """Scenario "{}" in the feature "{}" does not have valid examples. """ - """Set of step parameters {} should be a subset of example values {}.""".format( - self.name, self.feature.filename, sorted(params), sorted(example_params) + ( + """Scenario "{}" in the feature "{}" does not have valid examples. """ + """Set of step parameters {} should be a subset of example """ + """parameters {} if steps free variables are not allowed""" + ).format( + self.name, + self.feature.filename, + sorted(params), + sorted(example_params), ) ) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index c9446e38e..eabd10d70 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -215,11 +215,21 @@ def collect_example_parametrizations( return [pytest.param(context, id="-".join(context.values())) for context in contexts] -def scenario(feature_name: str, scenario_name: str, encoding: str = "utf-8", features_base_dir=None): +def scenario( + feature_name: str, + scenario_name: str, + *, + allow_example_free_variables=False, + allow_step_free_variables=True, + encoding: str = "utf-8", + features_base_dir=None, +): """Scenario decorator. :param str feature_name: Feature file name. Absolute or relative to the configured feature base path. :param str scenario_name: Scenario name. + :param allow_example_free_variables: Examples could contain free(unused) variables + :param allow_step_free_variables: Steps could contain free(unused) variables which could be taken from fixtures :param str encoding: Feature file encoding. """ @@ -234,6 +244,9 @@ def scenario(feature_name: str, scenario_name: str, encoding: str = "utf-8", fea # Get the scenario try: scenario = feature.scenarios[scenario_name] + scenario.allow_example_free_variables = allow_example_free_variables + scenario.allow_step_free_variables = allow_step_free_variables + except KeyError: feature_name = feature.name or "[Empty]" raise exceptions.ScenarioNotFound( @@ -294,10 +307,12 @@ def get_name(): suffix = f"_{index}" -def scenarios(*feature_paths, **kwargs): +def scenarios(*feature_paths, allow_example_free_variables=False, allow_step_free_variables=True, **kwargs): """Parse features from the paths and put all found scenarios in the caller module. :param *feature_paths: feature file paths to use for scenarios + :param allow_example_free_variables: Examples could contain free(unused) variables + :param allow_step_free_variables: Steps could contain free(unused) variables which could be taken from fixtures """ caller_locals = get_caller_module_locals() caller_path = get_caller_module_path() @@ -324,7 +339,13 @@ def scenarios(*feature_paths, **kwargs): # skip already bound scenarios if (scenario_object.feature.filename, scenario_name) not in module_scenarios: - @scenario(feature.filename, scenario_name, **kwargs) + @scenario( + feature.filename, + scenario_name, + allow_example_free_variables=allow_example_free_variables, + allow_step_free_variables=allow_step_free_variables, + **kwargs, + ) def _scenario(): pass # pragma: no cover diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 69e50e148..11f00b7ed 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -81,6 +81,54 @@ def test_outline(request): # fmt: on +def test_disallow_free_example_params(testdir): + """Test parametrized scenario when the test function lacks parameters.""" + + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: + | start | eat | left | unknown_param | + | 12 | 5 | 7 | value | + + """ + ), + ) + testdir.makeconftest(textwrap.dedent(STEPS)) + + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario + + @scenario( + "outline.feature", + "Outlined with wrong examples", + allow_example_free_variables=False + ) + def test_outline(request): + pass + """ + ) + ) + result = testdir.runpytest() + assert_outcomes(result, errors=1) + result.stdout.fnmatch_lines( + '*ScenarioExamplesNotValidError: Scenario "Outlined with wrong examples"*does not have valid examples*' + ) + result.stdout.fnmatch_lines( + "*Set of example parameters [[]'eat', 'left', 'start', 'unknown_param'[]] should be " + "a subset of step parameters [[]'eat', 'left', 'start'[]]*" + ) + + def test_outline_has_subset_of_parameters(testdir): """Test parametrized scenario when the test function has a subset of the parameters of the examples.""" @@ -108,8 +156,11 @@ def test_outline_has_subset_of_parameters(testdir): """\ from pytest_bdd import scenario - @scenario("outline.feature", "Outlined with subset of examples", - example_converters=dict(start=int, eat=float, left=str)) + @scenario( + "outline.feature", + "Outlined with subset of examples", + allow_example_free_variables=True + ) def test_outline(request): pass """ @@ -120,7 +171,8 @@ def test_outline(request): def test_wrongly_outlined_parameters_not_a_subset_of_examples(testdir): - """Test parametrized scenario when the test function has a parameter set which is not a subset of those in the examples table.""" + """Test parametrized scenario when the test function has a parameter set + which is not a subset of those in the examples table.""" testdir.makefile( ".feature", @@ -145,8 +197,9 @@ def test_wrongly_outlined_parameters_not_a_subset_of_examples(testdir): textwrap.dedent( """\ from pytest_bdd import scenario, then + import pytest_bdd.parsers as parsers - @scenario("outline.feature", "Outlined with wrong examples") + @scenario("outline.feature", "Outlined with wrong examples", allow_step_free_variables=False) def test_outline(request): pass @@ -161,7 +214,7 @@ def stepdef(left, right): result.stdout.fnmatch_lines( '*ScenarioExamplesNotValidError: Scenario "Outlined with wrong examples"*does not have valid examples*', ) - result.stdout.fnmatch_lines("*should be a subset of example values [[]'eat', 'left', 'start'[]].*") + result.stdout.fnmatch_lines("*should be a subset of example parameters [[]'eat', 'left', 'start'[]]*") def test_wrong_vertical_examples_scenario(testdir): diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index 7c766b42e..50a32a9d6 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -77,3 +77,74 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): 2, 1, 1, ] # fmt: on + + +def test_outlining_using_fixtures(testdir): + """Test parametrized scenario.""" + testdir.makefile( + ".feature", + parametrized=textwrap.dedent( + """\ + Feature: Parametrized scenario + Scenario: Parametrized given, when, thens + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + """ + ), + ) + + testdir.makepyfile( + textwrap.dedent( + """\ + import pytest + from pytest_bdd import given, when, then, scenario + + @pytest.fixture + def start(): + return 12 + + @pytest.fixture + def eat(): + return 5 + + @pytest.fixture + def left(): + return 7 + + + @pytest.fixture(params=[1, 2]) + def foo_bar(request): + return "bar" * request.param + + + @scenario("parametrized.feature", "Parametrized given, when, thens") + def test_parametrized(request, start, eat, left): + pass + + + @scenario("parametrized.feature", "Parametrized given, when, thens") + def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar): + pass + + @given("there are cucumbers", target_fixture="start_cucumbers") + def start_cucumbers(start): + return dict(start=start) + + + @when("I eat cucumbers") + def eat_cucumbers(start_cucumbers, start, eat): + start_cucumbers["eat"] = eat + + + @then("I should have cucumbers") + def should_have_left_cucumbers(start_cucumbers, start, eat, left): + assert start - eat == left + assert start_cucumbers["start"] == start + assert start_cucumbers["eat"] == eat + + """ + ) + ) + result = testdir.runpytest() + result.assert_outcomes(passed=3) From db3275cd1e0a8f8ed0c79b491f2a4fbc12409db2 Mon Sep 17 00:00:00 2001 From: elchupanebrej Date: Sun, 2 Jan 2022 15:05:28 +0200 Subject: [PATCH 4/6] Remove obsolete test because of #448 --- tests/feature/test_parametrized.py | 71 ------------------------------ 1 file changed, 71 deletions(-) diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index 50a32a9d6..7c766b42e 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -77,74 +77,3 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): 2, 1, 1, ] # fmt: on - - -def test_outlining_using_fixtures(testdir): - """Test parametrized scenario.""" - testdir.makefile( - ".feature", - parametrized=textwrap.dedent( - """\ - Feature: Parametrized scenario - Scenario: Parametrized given, when, thens - Given there are cucumbers - When I eat cucumbers - Then I should have cucumbers - """ - ), - ) - - testdir.makepyfile( - textwrap.dedent( - """\ - import pytest - from pytest_bdd import given, when, then, scenario - - @pytest.fixture - def start(): - return 12 - - @pytest.fixture - def eat(): - return 5 - - @pytest.fixture - def left(): - return 7 - - - @pytest.fixture(params=[1, 2]) - def foo_bar(request): - return "bar" * request.param - - - @scenario("parametrized.feature", "Parametrized given, when, thens") - def test_parametrized(request, start, eat, left): - pass - - - @scenario("parametrized.feature", "Parametrized given, when, thens") - def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar): - pass - - @given("there are cucumbers", target_fixture="start_cucumbers") - def start_cucumbers(start): - return dict(start=start) - - - @when("I eat cucumbers") - def eat_cucumbers(start_cucumbers, start, eat): - start_cucumbers["eat"] = eat - - - @then("I should have cucumbers") - def should_have_left_cucumbers(start_cucumbers, start, eat, left): - assert start - eat == left - assert start_cucumbers["start"] == start - assert start_cucumbers["eat"] == eat - - """ - ) - ) - result = testdir.runpytest() - result.assert_outcomes(passed=3) From ef31daa851a68c31667887c33fcd66e7e43f7a43 Mon Sep 17 00:00:00 2001 From: Kostiantyn Goloveshko Date: Sun, 2 Jan 2022 17:15:10 +0200 Subject: [PATCH 5/6] Provide ini options to control free variables in examples and steps --- README.rst | 38 ++++++ pytest_bdd/parser.py | 6 +- pytest_bdd/plugin.py | 14 +- pytest_bdd/scenario.py | 53 +++++--- tests/feature/test_outline.py | 248 +++++++++++++++++++++++++++++++++- 5 files changed, 331 insertions(+), 28 deletions(-) diff --git a/README.rst b/README.rst index 366f6327f..576d68581 100644 --- a/README.rst +++ b/README.rst @@ -699,6 +699,7 @@ The significant downside of this approach is inability to see the test table fro It's possible to disallow steps free parameters substitution from fixtures (so test case will fail): .. code-block:: python + @pytest.mark.parametrize( ["start", "eat", "left"], [(12, 5, 7)], @@ -714,6 +715,7 @@ It's possible to disallow steps free parameters substitution from fixtures (so t Sometimes you want leave a column not used in steps for specific reason in examples section: .. code-block:: gherkin + Feature: Scenario outlines Scenario Outline: Outlined given, when, thens Given there are cucumbers @@ -726,6 +728,7 @@ Sometimes you want leave a column not used in steps for specific reason in examp .. code-block:: python + from pytest_bdd import given, when, then, scenario @@ -737,6 +740,41 @@ Sometimes you want leave a column not used in steps for specific reason in examp def test_outlined(): pass +Or leave some parameter as is without substitution: + +.. code-block:: gherkin + + Feature: Outline + Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers in my bucket + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + +.. code-block:: python + + @scenario( + "outline.feature", + "Outlined with wrong examples", + allow_step_free_variables=False + ) + def test_outline(request): + pass + + @then(parsers.parse('I should have {left} cucumbers in my bucket')) + def stepdef(left): + pass + + +Also you could grant such possibility for whole session using pytest.ini (or any other same config) + +.. code-block:: ini + + [pytest] + bdd_allow_step_free_variables=false Organizing your scenarios ------------------------- diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 523b5cb11..a39e37868 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -401,7 +401,11 @@ def params(self): def render(self, context: typing.Mapping[str, typing.Any]): def replacer(m: typing.Match): varname = m.group(1) - return str(context[varname]) + try: + return str(context[varname]) + except KeyError: + # Unavailability of varname is handled by bdd_allow_step_free_variables ini option + return f"<{varname}>" return STEP_PARAM_RE.sub(replacer, self.name) diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index c6d42e8f9..460bce393 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -44,7 +44,19 @@ def pytest_addoption(parser): def add_bdd_ini(parser): - parser.addini("bdd_features_base_dir", "Base features directory.") + parser.addini(name="bdd_features_base_dir", help="Base features directory.") + parser.addini( + name="bdd_allow_step_free_variables", + help="Allow use not defined in examples. They will be skipped during parametrization", + type="bool", + default=False, + ) + parser.addini( + name="bdd_allow_example_free_variables", + help="Allow use not defined in steps. They will be skipped during parametrization", + type="bool", + default=True, + ) @pytest.mark.trylast diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index eabd10d70..90c875b0f 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -13,7 +13,9 @@ import collections import os import re +import sys import typing +from pathlib import Path import pytest from _pytest.fixtures import FixtureLookupError @@ -219,18 +221,19 @@ def scenario( feature_name: str, scenario_name: str, *, - allow_example_free_variables=False, - allow_step_free_variables=True, + allow_example_free_variables: typing.Optional[typing.Any] = None, + allow_step_free_variables: typing.Optional[typing.Any] = None, encoding: str = "utf-8", - features_base_dir=None, + features_base_dir: typing.Optional[typing.Union[str, Path]] = None, ): """Scenario decorator. - :param str feature_name: Feature file name. Absolute or relative to the configured feature base path. - :param str scenario_name: Scenario name. + :param feature_name: Feature file name. Absolute or relative to the configured feature base path. + :param scenario_name: Scenario name. :param allow_example_free_variables: Examples could contain free(unused) variables :param allow_step_free_variables: Steps could contain free(unused) variables which could be taken from fixtures - :param str encoding: Feature file encoding. + :param encoding: Feature file encoding. + :param features_base_dir: Base directory to build features path """ scenario_name = str(scenario_name) @@ -239,13 +242,21 @@ def scenario( # Get the feature if features_base_dir is None: features_base_dir = get_features_base_dir(caller_module_path) - feature = get_feature(features_base_dir, feature_name, encoding=encoding) + feature = get_feature(str(features_base_dir), feature_name, encoding=encoding) # Get the scenario try: scenario = feature.scenarios[scenario_name] - scenario.allow_example_free_variables = allow_example_free_variables - scenario.allow_step_free_variables = allow_step_free_variables + scenario.allow_example_free_variables = ( + allow_example_free_variables + if allow_example_free_variables is not None + else get_from_ini("bdd_allow_example_free_variables") + ) + scenario.allow_step_free_variables = ( + allow_step_free_variables + if allow_step_free_variables is not None + else get_from_ini("bdd_allow_step_free_variables") + ) except KeyError: feature_name = feature.name or "[Empty]" @@ -266,7 +277,7 @@ def get_features_base_dir(caller_module_path): return get_from_ini("bdd_features_base_dir", default_base_dir) -def get_from_ini(key, default): +def get_from_ini(key, default=None): """Get value from ini config. Return default if value has not been set. Use if the default value is dynamic. Otherwise set default on addini call. @@ -307,12 +318,16 @@ def get_name(): suffix = f"_{index}" -def scenarios(*feature_paths, allow_example_free_variables=False, allow_step_free_variables=True, **kwargs): +class ScenarioKwargs(typing.TypedDict if sys.version_info >= (3, 8) else object): + allow_example_free_variables: typing.Optional[typing.Any] + allow_step_free_variables: typing.Optional[typing.Any] + features_base_dir: typing.Optional[typing.Union[str, Path]] + + +def scenarios(*feature_paths: typing.Union[str, Path], **kwargs: ScenarioKwargs): """Parse features from the paths and put all found scenarios in the caller module. - :param *feature_paths: feature file paths to use for scenarios - :param allow_example_free_variables: Examples could contain free(unused) variables - :param allow_step_free_variables: Steps could contain free(unused) variables which could be taken from fixtures + :param feature_paths: feature file paths to use for scenarios """ caller_locals = get_caller_module_locals() caller_path = get_caller_module_path() @@ -324,7 +339,7 @@ def scenarios(*feature_paths, allow_example_free_variables=False, allow_step_fre abs_feature_paths = [] for path in feature_paths: if not os.path.isabs(path): - path = os.path.abspath(os.path.join(features_base_dir, path)) + path = os.path.abspath(os.path.join(str(features_base_dir), path)) abs_feature_paths.append(path) found = False @@ -339,13 +354,7 @@ def scenarios(*feature_paths, allow_example_free_variables=False, allow_step_fre # skip already bound scenarios if (scenario_object.feature.filename, scenario_name) not in module_scenarios: - @scenario( - feature.filename, - scenario_name, - allow_example_free_variables=allow_example_free_variables, - allow_step_free_variables=allow_step_free_variables, - **kwargs, - ) + @scenario(feature.filename, scenario_name, **kwargs) def _scenario(): pass # pragma: no cover diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 11f00b7ed..d27a2fb49 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -129,7 +129,57 @@ def test_outline(request): ) -def test_outline_has_subset_of_parameters(testdir): +def test_disallow_free_example_params_by_ini(testdir): + """Test parametrized scenario when the test function lacks parameters.""" + testdir.makeini( + """ + [pytest] + bdd_allow_example_free_variables=false + """ + ) + + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: + | start | eat | left | unknown_param | + | 12 | 5 | 7 | value | + + """ + ), + ) + testdir.makeconftest(textwrap.dedent(STEPS)) + + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario + + @scenario("outline.feature", "Outlined with wrong examples") + def test_outline(request): + pass + """ + ) + ) + result = testdir.runpytest() + assert_outcomes(result, errors=1) + result.stdout.fnmatch_lines( + '*ScenarioExamplesNotValidError: Scenario "Outlined with wrong examples"*does not have valid examples*' + ) + result.stdout.fnmatch_lines( + "*Set of example parameters [[]'eat', 'left', 'start', 'unknown_param'[]] should be " + "a subset of step parameters [[]'eat', 'left', 'start'[]]*" + ) + + +def test_allow_free_example_params(testdir): """Test parametrized scenario when the test function has a subset of the parameters of the examples.""" testdir.makefile( @@ -170,7 +220,51 @@ def test_outline(request): assert_outcomes(result, passed=1) -def test_wrongly_outlined_parameters_not_a_subset_of_examples(testdir): +def test_allow_free_example_params_by_ini(testdir): + """Test parametrized scenario when the test function has a subset of the parameters of the examples.""" + + testdir.makeini( + """ + [pytest] + bdd_allow_example_free_variables=true + """ + ) + + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Scenario Outline: Outlined with subset of examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: + | start | eat | left | notes | + | 12 | 5 | 7 | Should be ignored | + + """ + ), + ) + testdir.makeconftest(textwrap.dedent(STEPS)) + + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario + + @scenario("outline.feature", "Outlined with subset of examples") + def test_outline(request): + pass + """ + ) + ) + result = testdir.runpytest() + assert_outcomes(result, passed=1) + + +def test_disallow_outlined_parameters_not_a_subset_of_examples(testdir): """Test parametrized scenario when the test function has a parameter set which is not a subset of those in the examples table.""" @@ -203,8 +297,61 @@ def test_wrongly_outlined_parameters_not_a_subset_of_examples(testdir): def test_outline(request): pass - @then(parsers.parse('I should have cucumbers in my bucket')) - def stepdef(left, right): + @then(parsers.parse('I should have {left} cucumbers in my bucket')) + def stepdef(left): + pass + """ + ) + ) + result = testdir.runpytest() + assert_outcomes(result, errors=1) + result.stdout.fnmatch_lines( + '*ScenarioExamplesNotValidError: Scenario "Outlined with wrong examples"*does not have valid examples*', + ) + result.stdout.fnmatch_lines("*should be a subset of example parameters [[]'eat', 'left', 'start'[]]*") + + +def test_disallow_outlined_parameters_not_a_subset_of_examples_by_ini(testdir): + """Test parametrized scenario when the test function has a parameter set + which is not a subset of those in the examples table.""" + testdir.makeini( + """ + [pytest] + bdd_allow_step_free_variables=false + """ + ) + + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers in my bucket + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + + """ + ), + ) + testdir.makeconftest(textwrap.dedent(STEPS)) + + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario, then + import pytest_bdd.parsers as parsers + + @scenario("outline.feature", "Outlined with wrong examples") + def test_outline(request): + pass + + @then(parsers.parse('I should have {left} cucumbers in my bucket')) + def stepdef(left): pass """ ) @@ -217,6 +364,99 @@ def stepdef(left, right): result.stdout.fnmatch_lines("*should be a subset of example parameters [[]'eat', 'left', 'start'[]]*") +def test_allow_outlined_parameters_not_a_subset_of_examples(testdir): + """Test parametrized scenario when the test function has a parameter set + which is not a subset of those in the examples table.""" + + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers in my bucket + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + + """ + ), + ) + testdir.makeconftest(textwrap.dedent(STEPS)) + + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario, then + import pytest_bdd.parsers as parsers + + @scenario("outline.feature", "Outlined with wrong examples", allow_step_free_variables=True) + def test_outline(request): + pass + + @then(parsers.parse('I should have {left} cucumbers in my bucket')) + def stepdef(left): + pass + """ + ) + ) + result = testdir.runpytest() + assert_outcomes(result, passed=1) + + +def test_allow_outlined_parameters_not_a_subset_of_examples_by_ini(testdir): + """Test parametrized scenario when the test function has a parameter set + which is not a subset of those in the examples table.""" + + testdir.makeini( + """ + [pytest] + bdd_allow_step_free_variables=true + """ + ) + + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers in my bucket + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + + """ + ), + ) + testdir.makeconftest(textwrap.dedent(STEPS)) + + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario, then + import pytest_bdd.parsers as parsers + + @scenario("outline.feature", "Outlined with wrong examples") + def test_outline(request): + pass + + @then(parsers.parse('I should have {left} cucumbers in my bucket')) + def stepdef(left): + pass + """ + ) + ) + result = testdir.runpytest() + assert_outcomes(result, passed=1) + + def test_wrong_vertical_examples_scenario(testdir): """Test parametrized scenario vertical example table has wrong format.""" testdir.makefile( From 6be32fb560e2b9cb199c21a80747b79ae2210a5e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 2 Jan 2022 15:15:36 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/feature/test_outline.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index d27a2fb49..6c0c0ca41 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -109,7 +109,7 @@ def test_disallow_free_example_params(testdir): from pytest_bdd import scenario @scenario( - "outline.feature", + "outline.feature", "Outlined with wrong examples", allow_example_free_variables=False ) @@ -207,8 +207,8 @@ def test_allow_free_example_params(testdir): from pytest_bdd import scenario @scenario( - "outline.feature", - "Outlined with subset of examples", + "outline.feature", + "Outlined with subset of examples", allow_example_free_variables=True ) def test_outline(request): @@ -296,7 +296,7 @@ def test_disallow_outlined_parameters_not_a_subset_of_examples(testdir): @scenario("outline.feature", "Outlined with wrong examples", allow_step_free_variables=False) def test_outline(request): pass - + @then(parsers.parse('I should have {left} cucumbers in my bucket')) def stepdef(left): pass @@ -349,7 +349,7 @@ def test_disallow_outlined_parameters_not_a_subset_of_examples_by_ini(testdir): @scenario("outline.feature", "Outlined with wrong examples") def test_outline(request): pass - + @then(parsers.parse('I should have {left} cucumbers in my bucket')) def stepdef(left): pass