Skip to content

Commit 6e4d4c6

Browse files
Kostiantyn GoloveshkoKostiantyn Goloveshko
authored andcommitted
Allow explicit free variables control at Examples and steps
1 parent 25732b5 commit 6e4d4c6

File tree

6 files changed

+230
-10
lines changed

6 files changed

+230
-10
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Changelog
33

44
Unreleased
55
-----------
6+
- Add options to control free variables in Examples and step definitions
67

78
4.1.0
89
-----------

README.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,46 @@ With a parametrized.feature file:
701701
702702
703703
The significant downside of this approach is inability to see the test table from the feature file.
704+
It's possible to disallow steps free parameters substitution from fixtures (so test case will fail):
705+
706+
.. code-block:: python
707+
@pytest.mark.parametrize(
708+
["start", "eat", "left"],
709+
[(12, 5, 7)],
710+
)
711+
@scenario(
712+
"parametrized.feature",
713+
"Parametrized given, when, thens",
714+
allow_step_free_variables=False,
715+
)
716+
def test_parametrized(start, eat, left):
717+
"""We don't need to do anything here, everything will be managed by the scenario decorator."""
718+
719+
Sometimes you want leave a column not used in steps for specific reason in examples section:
720+
721+
.. code-block:: gherkin
722+
Feature: Scenario outlines
723+
Scenario Outline: Outlined given, when, thens
724+
Given there are <start> cucumbers
725+
When I eat <eat> cucumbers
726+
Then I should have <left> cucumbers
727+
728+
Examples:
729+
| start | eat | left | comment |
730+
| 12 | 5 | 7 | sweet cucumbers!|
731+
732+
733+
.. code-block:: python
734+
from pytest_bdd import given, when, then, scenario
735+
736+
737+
@scenario(
738+
"outline.feature",
739+
"Outlined given, when, thens",
740+
allow_example_free_variables=True,
741+
)
742+
def test_outlined():
743+
pass
704744
705745
706746
Organizing your scenarios

pytest_bdd/parser.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ def __init__(self, feature, name, line_number, example_converters=None, tags=Non
236236
self.failed = False
237237
self.test_function = None
238238

239+
self.allow_example_free_variables = None
240+
self.allow_step_free_variables = None
241+
239242
def add_step(self, step):
240243
"""Add step to the scenario.
241244
@@ -281,11 +284,45 @@ def validate(self):
281284
"""
282285
params = self.params
283286
example_params = self.get_example_params()
284-
if params and example_params and not params.issubset(example_params):
287+
if params or example_params:
288+
self._validate_example_free_variables()
289+
self._validate_step_free_variables()
290+
291+
def _validate_example_free_variables(self):
292+
params = self.params
293+
example_params = self.get_example_params()
294+
if self.allow_example_free_variables or example_params.issubset(params):
295+
return
296+
else:
297+
raise exceptions.ScenarioExamplesNotValidError(
298+
(
299+
"""Scenario "{}" in the feature "{}" does not have valid examples. """
300+
"""Set of example parameters {} should be a subset of step """
301+
"""parameters {} if examples free variables are not allowed"""
302+
).format(
303+
self.name,
304+
self.feature.filename,
305+
sorted(example_params),
306+
sorted(params),
307+
)
308+
)
309+
310+
def _validate_step_free_variables(self):
311+
params = self.params
312+
example_params = self.get_example_params()
313+
if self.allow_step_free_variables or params.issubset(example_params):
314+
return
315+
else:
285316
raise exceptions.ScenarioExamplesNotValidError(
286-
"""Scenario "{}" in the feature "{}" does not have valid examples. """
287-
"""Set of step parameters {} should be a subset of example values {}.""".format(
288-
self.name, self.feature.filename, sorted(params), sorted(example_params)
317+
(
318+
"""Scenario "{}" in the feature "{}" does not have valid examples. """
319+
"""Set of step parameters {} should be a subset of example """
320+
"""parameters {} if steps free variables are not allowed"""
321+
).format(
322+
self.name,
323+
self.feature.filename,
324+
sorted(params),
325+
sorted(example_params),
289326
)
290327
)
291328

pytest_bdd/scenario.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,22 @@ def scenario_wrapper(request):
180180
return decorator
181181

182182

183-
def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=None, features_base_dir=None):
183+
def scenario(
184+
feature_name,
185+
scenario_name,
186+
*,
187+
allow_example_free_variables=False,
188+
allow_step_free_variables=True,
189+
encoding="utf-8",
190+
example_converters=None,
191+
features_base_dir=None,
192+
):
184193
"""Scenario decorator.
185194
186195
:param str feature_name: Feature file name. Absolute or relative to the configured feature base path.
187196
:param str scenario_name: Scenario name.
197+
:param allow_example_free_variables: Examples could contain free(unused) variables
198+
:param allow_step_free_variables: Steps could contain free(unused) variables which could be taken from fixtures
188199
:param str encoding: Feature file encoding.
189200
:param dict example_converters: optional `dict` of example converter function, where key is the name of the
190201
example parameter, and value is the converter function.
@@ -201,6 +212,9 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
201212
# Get the scenario
202213
try:
203214
scenario = feature.scenarios[scenario_name]
215+
scenario.allow_example_free_variables = allow_example_free_variables
216+
scenario.allow_step_free_variables = allow_step_free_variables
217+
204218
except KeyError:
205219
feature_name = feature.name or "[Empty]"
206220
raise exceptions.ScenarioNotFound(
@@ -263,10 +277,12 @@ def get_name():
263277
suffix = f"_{index}"
264278

265279

266-
def scenarios(*feature_paths, **kwargs):
280+
def scenarios(*feature_paths, allow_example_free_variables=False, allow_step_free_variables=True, **kwargs):
267281
"""Parse features from the paths and put all found scenarios in the caller module.
268282
269283
:param *feature_paths: feature file paths to use for scenarios
284+
:param allow_example_free_variables: Examples could contain free(unused) variables
285+
:param allow_step_free_variables: Steps could contain free(unused) variables which could be taken from fixtures
270286
"""
271287
caller_locals = get_caller_module_locals()
272288
caller_path = get_caller_module_path()
@@ -293,7 +309,13 @@ def scenarios(*feature_paths, **kwargs):
293309
# skip already bound scenarios
294310
if (scenario_object.feature.filename, scenario_name) not in module_scenarios:
295311

296-
@scenario(feature.filename, scenario_name, **kwargs)
312+
@scenario(
313+
feature.filename,
314+
scenario_name,
315+
allow_example_free_variables=allow_example_free_variables,
316+
allow_step_free_variables=allow_step_free_variables,
317+
**kwargs,
318+
)
297319
def _scenario():
298320
pass # pragma: no cover
299321

tests/feature/test_outline.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,54 @@ def test_outline(request):
7878
result.assert_outcomes(passed=2)
7979

8080

81+
def test_disallow_free_example_params(testdir):
82+
"""Test parametrized scenario when the test function lacks parameters."""
83+
84+
testdir.makefile(
85+
".feature",
86+
outline=textwrap.dedent(
87+
"""\
88+
Feature: Outline
89+
Scenario Outline: Outlined with wrong examples
90+
Given there are <start> cucumbers
91+
When I eat <eat> cucumbers
92+
Then I should have <left> cucumbers
93+
94+
Examples:
95+
| start | eat | left | unknown_param |
96+
| 12 | 5 | 7 | value |
97+
98+
"""
99+
),
100+
)
101+
testdir.makeconftest(textwrap.dedent(STEPS))
102+
103+
testdir.makepyfile(
104+
textwrap.dedent(
105+
"""\
106+
from pytest_bdd import scenario
107+
108+
@scenario(
109+
"outline.feature",
110+
"Outlined with wrong examples",
111+
allow_example_free_variables=False
112+
)
113+
def test_outline(request):
114+
pass
115+
"""
116+
)
117+
)
118+
result = testdir.runpytest()
119+
assert_outcomes(result, errors=1)
120+
result.stdout.fnmatch_lines(
121+
'*ScenarioExamplesNotValidError: Scenario "Outlined with wrong examples"*does not have valid examples*'
122+
)
123+
result.stdout.fnmatch_lines(
124+
"*Set of example parameters [[]'eat', 'left', 'start', 'unknown_param'[]] should be "
125+
"a subset of step parameters [[]'eat', 'left', 'start'[]]*"
126+
)
127+
128+
81129
def test_outline_has_subset_of_parameters(testdir):
82130
"""Test parametrized scenario when the test function has a subset of the parameters of the examples."""
83131

@@ -106,7 +154,7 @@ def test_outline_has_subset_of_parameters(testdir):
106154
from pytest_bdd import scenario
107155
108156
@scenario("outline.feature", "Outlined with subset of examples",
109-
example_converters=dict(start=int, eat=float, left=str))
157+
example_converters=dict(start=int, eat=float, left=str), allow_example_free_variables=True)
110158
def test_outline(request):
111159
pass
112160
"""
@@ -142,8 +190,9 @@ def test_wrongly_outlined_parameters_not_a_subset_of_examples(testdir):
142190
textwrap.dedent(
143191
"""\
144192
from pytest_bdd import scenario, then
193+
import pytest_bdd.parsers as parsers
145194
146-
@scenario("outline.feature", "Outlined with wrong examples")
195+
@scenario("outline.feature", "Outlined with wrong examples", allow_step_free_variables=False)
147196
def test_outline(request):
148197
pass
149198
@@ -158,7 +207,7 @@ def stepdef(left, right):
158207
result.stdout.fnmatch_lines(
159208
'*ScenarioExamplesNotValidError: Scenario "Outlined with wrong examples"*does not have valid examples*',
160209
)
161-
result.stdout.fnmatch_lines("*should be a subset of example values [[]'eat', 'left', 'start'[]].*")
210+
result.stdout.fnmatch_lines("*should be a subset of example parameters [[]'eat', 'left', 'start'[]]*")
162211

163212

164213
def test_wrong_vertical_examples_scenario(testdir):

tests/feature/test_parametrized.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,74 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left):
5959
)
6060
result = testdir.runpytest()
6161
result.assert_outcomes(passed=3)
62+
63+
64+
def test_outlining_using_fixtures(testdir):
65+
"""Test parametrized scenario."""
66+
testdir.makefile(
67+
".feature",
68+
parametrized=textwrap.dedent(
69+
"""\
70+
Feature: Parametrized scenario
71+
Scenario: Parametrized given, when, thens
72+
Given there are <start> cucumbers
73+
When I eat <eat> cucumbers
74+
Then I should have <left> cucumbers
75+
"""
76+
),
77+
)
78+
79+
testdir.makepyfile(
80+
textwrap.dedent(
81+
"""\
82+
import pytest
83+
from pytest_bdd import given, when, then, scenario
84+
85+
@pytest.fixture
86+
def start():
87+
return 12
88+
89+
@pytest.fixture
90+
def eat():
91+
return 5
92+
93+
@pytest.fixture
94+
def left():
95+
return 7
96+
97+
98+
@pytest.fixture(params=[1, 2])
99+
def foo_bar(request):
100+
return "bar" * request.param
101+
102+
103+
@scenario("parametrized.feature", "Parametrized given, when, thens")
104+
def test_parametrized(request, start, eat, left):
105+
pass
106+
107+
108+
@scenario("parametrized.feature", "Parametrized given, when, thens")
109+
def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar):
110+
pass
111+
112+
@given("there are <start> cucumbers", target_fixture="start_cucumbers")
113+
def start_cucumbers(start):
114+
return dict(start=start)
115+
116+
117+
@when("I eat <eat> cucumbers")
118+
def eat_cucumbers(start_cucumbers, start, eat):
119+
start_cucumbers["eat"] = eat
120+
121+
122+
@then("I should have <left> cucumbers")
123+
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
124+
assert start - eat == left
125+
assert start_cucumbers["start"] == start
126+
assert start_cucumbers["eat"] == eat
127+
128+
"""
129+
)
130+
)
131+
result = testdir.runpytest()
132+
result.assert_outcomes(passed=3)

0 commit comments

Comments
 (0)