Skip to content

Commit 150e079

Browse files
authored
Merge branch 'master' into ab/fix-typing
2 parents 5e5c60f + 6cdd340 commit 150e079

File tree

8 files changed

+192
-55
lines changed

8 files changed

+192
-55
lines changed

Diff for: CHANGES.rst

+23-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,29 @@ Added
1414

1515
Changed
1616
+++++++
17-
* Step arguments ``"datatable"`` and ``"docstring"`` are now reserved, and they can't be used as step argument names.
17+
18+
Deprecated
19+
++++++++++
20+
21+
Removed
22+
+++++++
23+
24+
Fixed
25+
+++++
26+
27+
Security
28+
++++++++
29+
30+
[8.1.0] - 2024-12-05
31+
----------
32+
33+
Added
34+
+++++
35+
36+
Changed
37+
+++++++
38+
* Step arguments ``"datatable"`` and ``"docstring"`` are now reserved, and they can't be used as step argument names. An error is raised if a step parser uses these names.
39+
* Scenario ``description`` field is now set for Cucumber JSON output.
1840

1941
Deprecated
2042
++++++++++

Diff for: README.rst

+85-33
Original file line numberDiff line numberDiff line change
@@ -565,39 +565,6 @@ Example:
565565
assert datatable[1][1] in ["user1", "user2"]
566566
567567
568-
Rules
569-
-----
570-
571-
In Gherkin, `Rules` allow you to group related scenarios or examples under a shared context.
572-
This is useful when you want to define different conditions or behaviours
573-
for multiple examples that follow a similar structure.
574-
You can use either ``Scenario`` or ``Example`` to define individual cases, as they are aliases and function identically.
575-
576-
Additionally, **tags** applied to a rule will be automatically applied to all the **examples or scenarios**
577-
under that rule, making it easier to organize and filter tests during execution.
578-
579-
Example:
580-
581-
.. code-block:: gherkin
582-
583-
Feature: Rules and examples
584-
585-
@feature_tag
586-
Rule: A rule for valid cases
587-
588-
@rule_tag
589-
Example: Valid case 1
590-
Given I have a valid input
591-
When I process the input
592-
Then the result should be successful
593-
594-
Rule: A rule for invalid cases
595-
Example: Invalid case
596-
Given I have an invalid input
597-
When I process the input
598-
Then the result should be an error
599-
600-
601568
Scenario Outlines with Multiple Example Tables
602569
----------------------------------------------
603570

@@ -663,6 +630,91 @@ only the examples under the "Positive results" table will be executed, and the "
663630
pytest -k "positive"
664631
665632
633+
Handling Empty Example Cells
634+
----------------------------
635+
636+
By default, empty cells in the example tables are interpreted as empty strings ("").
637+
However, there may be cases where it is more appropriate to handle them as ``None``.
638+
In such scenarios, you can use a converter with the ``parsers.re`` parser to define a custom behavior for empty values.
639+
640+
For example, the following code demonstrates how to use a custom converter to return ``None`` when an empty cell is encountered:
641+
642+
.. code-block:: gherkin
643+
644+
# content of empty_example_cells.feature
645+
646+
Feature: Handling empty example cells
647+
Scenario Outline: Using converters for empty cells
648+
Given I am starting lunch
649+
Then there are <start> cucumbers
650+
651+
Examples:
652+
| start |
653+
| |
654+
655+
.. code-block:: python
656+
657+
from pytest_bdd import then, parsers
658+
659+
660+
# Define a converter that returns None for empty strings
661+
def empty_to_none(value):
662+
return None if value.strip() == "" else value
663+
664+
665+
@given("I am starting lunch")
666+
def _():
667+
pass
668+
669+
670+
@then(
671+
parsers.re("there are (?P<start>.*?) cucumbers"),
672+
converters={"start": empty_to_none}
673+
)
674+
def _(start):
675+
# Example assertion to demonstrate the conversion
676+
assert start is None
677+
678+
679+
Here, the `start` cell in the example table is empty.
680+
When the ``parsers.re`` parser is combined with the ``empty_to_none`` converter,
681+
the empty cell will be converted to ``None`` and can be handled accordingly in the step definition.
682+
683+
684+
Rules
685+
-----
686+
687+
In Gherkin, `Rules` allow you to group related scenarios or examples under a shared context.
688+
This is useful when you want to define different conditions or behaviours
689+
for multiple examples that follow a similar structure.
690+
You can use either ``Scenario`` or ``Example`` to define individual cases, as they are aliases and function identically.
691+
692+
Additionally, **tags** applied to a rule will be automatically applied to all the **examples or scenarios**
693+
under that rule, making it easier to organize and filter tests during execution.
694+
695+
Example:
696+
697+
.. code-block:: gherkin
698+
699+
Feature: Rules and examples
700+
701+
@feature_tag
702+
Rule: A rule for valid cases
703+
704+
@rule_tag
705+
Example: Valid case 1
706+
Given I have a valid input
707+
When I process the input
708+
Then the result should be successful
709+
710+
Rule: A rule for invalid cases
711+
712+
Example: Invalid case
713+
Given I have an invalid input
714+
When I process the input
715+
Then the result should be an error
716+
717+
666718
Datatables
667719
----------
668720

Diff for: pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pytest-bdd"
3-
version = "8.0.0"
3+
version = "8.1.0"
44
description = "BDD for pytest"
55
authors = ["Oleg Pidsadnyi <[email protected]>", "Anatoly Bubenkov <[email protected]>"]
66
maintainers = ["Alessio Bogon <[email protected]>"]

Diff for: src/pytest_bdd/cucumber_json.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def stepmap(step: StepReportDict) -> StepElementDict:
189189
"id": test_report_context_registry[report].name,
190190
"name": scenario["name"],
191191
"line": scenario["line_number"],
192-
"description": "",
192+
"description": scenario["description"],
193193
"tags": self._serialize_tags(scenario),
194194
"type": "scenario",
195195
"steps": [stepmap(step) for step in scenario["steps"]],

Diff for: src/pytest_bdd/reporting.py

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def serialize(self) -> ScenarioReportDict:
154154
"name": scenario.name,
155155
"line_number": scenario.line_number,
156156
"tags": sorted(scenario.tags),
157+
"description": scenario.description,
157158
"feature": {
158159
"keyword": feature.keyword,
159160
"name": feature.name,

Diff for: tests/feature/test_cucumber_json.py

+22-19
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,12 @@ def test_step_trace(pytester):
5151
"""
5252
@feature-tag
5353
Feature: One passing scenario, one failing scenario
54+
This is a feature description
5455
5556
@scenario-passing-tag
5657
Scenario: Passing
58+
This is a scenario description
59+
5760
Given a passing step
5861
And some other passing step
5962
@@ -116,108 +119,108 @@ def test_passing_outline():
116119
assert result.ret
117120
expected = [
118121
{
119-
"description": "",
122+
"description": "This is a feature description",
120123
"elements": [
121124
{
122-
"description": "",
125+
"description": "This is a scenario description",
123126
"id": "test_passing",
124127
"keyword": "Scenario",
125-
"line": 5,
128+
"line": 6,
126129
"name": "Passing",
127130
"steps": [
128131
{
129132
"keyword": "Given",
130-
"line": 6,
133+
"line": 9,
131134
"match": {"location": ""},
132135
"name": "a passing step",
133136
"result": {"status": "passed", "duration": OfType(int)},
134137
},
135138
{
136139
"keyword": "And",
137-
"line": 7,
140+
"line": 10,
138141
"match": {"location": ""},
139142
"name": "some other passing step",
140143
"result": {"status": "passed", "duration": OfType(int)},
141144
},
142145
],
143-
"tags": [{"name": "scenario-passing-tag", "line": 4}],
146+
"tags": [{"name": "scenario-passing-tag", "line": 5}],
144147
"type": "scenario",
145148
},
146149
{
147150
"description": "",
148151
"id": "test_failing",
149152
"keyword": "Scenario",
150-
"line": 10,
153+
"line": 13,
151154
"name": "Failing",
152155
"steps": [
153156
{
154157
"keyword": "Given",
155-
"line": 11,
158+
"line": 14,
156159
"match": {"location": ""},
157160
"name": "a passing step",
158161
"result": {"status": "passed", "duration": OfType(int)},
159162
},
160163
{
161164
"keyword": "And",
162-
"line": 12,
165+
"line": 15,
163166
"match": {"location": ""},
164167
"name": "a failing step",
165168
"result": {"error_message": OfType(str), "status": "failed", "duration": OfType(int)},
166169
},
167170
],
168-
"tags": [{"name": "scenario-failing-tag", "line": 9}],
171+
"tags": [{"name": "scenario-failing-tag", "line": 12}],
169172
"type": "scenario",
170173
},
171174
{
172175
"description": "",
173176
"keyword": "Scenario Outline",
174-
"tags": [{"line": 14, "name": "scenario-outline-passing-tag"}],
177+
"tags": [{"line": 17, "name": "scenario-outline-passing-tag"}],
175178
"steps": [
176179
{
177-
"line": 16,
180+
"line": 19,
178181
"match": {"location": ""},
179182
"result": {"status": "passed", "duration": OfType(int)},
180183
"keyword": "Given",
181184
"name": "type str and value hello",
182185
}
183186
],
184-
"line": 15,
187+
"line": 18,
185188
"type": "scenario",
186189
"id": "test_passing_outline[str-hello]",
187190
"name": "Passing outline",
188191
},
189192
{
190193
"description": "",
191194
"keyword": "Scenario Outline",
192-
"tags": [{"line": 14, "name": "scenario-outline-passing-tag"}],
195+
"tags": [{"line": 17, "name": "scenario-outline-passing-tag"}],
193196
"steps": [
194197
{
195-
"line": 16,
198+
"line": 19,
196199
"match": {"location": ""},
197200
"result": {"status": "passed", "duration": OfType(int)},
198201
"keyword": "Given",
199202
"name": "type int and value 42",
200203
}
201204
],
202-
"line": 15,
205+
"line": 18,
203206
"type": "scenario",
204207
"id": "test_passing_outline[int-42]",
205208
"name": "Passing outline",
206209
},
207210
{
208211
"description": "",
209212
"keyword": "Scenario Outline",
210-
"tags": [{"line": 14, "name": "scenario-outline-passing-tag"}],
213+
"tags": [{"line": 17, "name": "scenario-outline-passing-tag"}],
211214
"steps": [
212215
{
213-
"line": 16,
216+
"line": 19,
214217
"match": {"location": ""},
215218
"result": {"status": "passed", "duration": OfType(int)},
216219
"keyword": "Given",
217220
"name": "type float and value 1.0",
218221
}
219222
],
220-
"line": 15,
223+
"line": 18,
221224
"type": "scenario",
222225
"id": "test_passing_outline[float-1.0]",
223226
"name": "Passing outline",

Diff for: tests/feature/test_outline.py

+55
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,58 @@ def _(site):
320320
result = pytester.runpytest("-s")
321321
result.assert_outcomes(passed=1)
322322
assert collect_dumped_objects(result) == ["https://my-site.com"]
323+
324+
325+
def test_variable_reuse(pytester):
326+
"""
327+
Test example parameter name and step arg do not redefine each other's value
328+
if the same name is used for both in different steps.
329+
"""
330+
331+
pytester.makefile(
332+
".feature",
333+
outline=textwrap.dedent(
334+
"""\
335+
Feature: Example parameters reuse
336+
Scenario Outline: Check for example parameter re-use
337+
Given the param is initially set from the example table as <param>
338+
When a step arg of the same name is set to "other"
339+
Then the param is still set from the example table as <param>
340+
341+
Examples:
342+
| param |
343+
| value |
344+
345+
"""
346+
),
347+
)
348+
349+
pytester.makepyfile(
350+
textwrap.dedent(
351+
"""\
352+
from pytest_bdd import given, when, then, parsers, scenarios
353+
from pytest_bdd.utils import dump_obj
354+
355+
scenarios('outline.feature')
356+
357+
358+
@given(parsers.parse('the param is initially set from the example table as {param}'))
359+
def _(param):
360+
dump_obj(("param1", param))
361+
362+
363+
@when(parsers.re('a step arg of the same name is set to "(?P<param>.+)"'))
364+
def _(param):
365+
dump_obj(("param2", param))
366+
367+
368+
@then(parsers.parse('the param is still set from the example table as {param}'))
369+
def _(param):
370+
dump_obj(("param3", param))
371+
372+
"""
373+
)
374+
)
375+
result = pytester.runpytest("-s")
376+
result.assert_outcomes(passed=1)
377+
assert collect_dumped_objects(result) == [("param1", "value"), ("param2", "other"), ("param3", "value")]

0 commit comments

Comments
 (0)