Skip to content

Commit b5f90f6

Browse files
authored
Merge pull request #600 from pbarnajc/scenario-descriptions
Add support for Scenario descriptions
2 parents d71bb90 + ad6df6e commit b5f90f6

File tree

3 files changed

+54
-6
lines changed

3 files changed

+54
-6
lines changed

CHANGES.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Changelog
44
Unreleased
55
----------
66
- ⚠️ Backwards incompatible: - ``parsers.re`` now does a `fullmatch <https://docs.python.org/3/library/re.html#re.fullmatch>`_ instead of a partial match. This is to make it work just like the other parsers, since they don't ignore non-matching characters at the end of the string. `#539 <https://github.com/pytest-dev/pytest-bdd/pull/539>`_
7-
7+
- Add support for Scenarios and Scenario Outlines to have descriptions. `#600 <https://github.com/pytest-dev/pytest-bdd/pull/600>`_
88

99
6.1.1
1010
-----

src/pytest_bdd/parser.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
("But ", None),
2929
]
3030

31+
TYPES_WITH_DESCRIPTIONS = [types.FEATURE, types.SCENARIO, types.SCENARIO_OUTLINE]
32+
3133
if typing.TYPE_CHECKING:
3234
from typing import Any, Iterable, Mapping, Match, Sequence
3335

@@ -125,7 +127,8 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
125127
multiline_step = False
126128
stripped_line = line.strip()
127129
clean_line = strip_comments(line)
128-
if not clean_line and (not prev_mode or prev_mode not in types.FEATURE):
130+
if not clean_line and (not prev_mode or prev_mode not in TYPES_WITH_DESCRIPTIONS):
131+
# Blank lines are included in feature and scenario descriptions
129132
continue
130133
mode = get_step_type(clean_line) or mode
131134

@@ -142,7 +145,9 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
142145
feature.line_number = line_number
143146
feature.tags = get_tags(prev_line)
144147
elif prev_mode == types.FEATURE:
145-
description.append(clean_line)
148+
# Do not include comments in descriptions
149+
if not stripped_line.startswith("#"):
150+
description.append(clean_line)
146151
else:
147152
raise exceptions.FeatureError(
148153
"Multiple features are not allowed in a single feature file",
@@ -157,6 +162,14 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu
157162
keyword, parsed_line = parse_line(clean_line)
158163

159164
if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
165+
# Lines between the scenario declaration
166+
# and the scenario's first step line
167+
# are considered part of the scenario description.
168+
if scenario and not keyword:
169+
# Do not include comments in descriptions
170+
if not stripped_line.startswith("#"):
171+
scenario.add_description_line(clean_line)
172+
continue
160173
tags = get_tags(prev_line)
161174
scenario = ScenarioTemplate(
162175
feature=feature,
@@ -215,6 +228,7 @@ class ScenarioTemplate:
215228
tags: set[str] = field(default_factory=set)
216229
examples: Examples | None = field(default_factory=lambda: Examples())
217230
_steps: list[Step] = field(init=False, default_factory=list)
231+
_description_lines: list[str] = field(init=False, default_factory=list)
218232

219233
def add_step(self, step: Step) -> None:
220234
step.scenario = self
@@ -241,7 +255,27 @@ def render(self, context: Mapping[str, Any]) -> Scenario:
241255
for step in self._steps
242256
]
243257
steps = background_steps + scenario_steps
244-
return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags)
258+
return Scenario(
259+
feature=self.feature,
260+
name=self.name,
261+
line_number=self.line_number,
262+
steps=steps,
263+
tags=self.tags,
264+
description=self._description_lines,
265+
)
266+
267+
def add_description_line(self, description_line):
268+
"""Add a description line to the scenario.
269+
:param str description_line:
270+
"""
271+
self._description_lines.append(description_line)
272+
273+
@property
274+
def description(self):
275+
"""Get the scenario's description.
276+
:return: The scenario description
277+
"""
278+
return "\n".join(self._description_lines)
245279

246280

247281
@dataclass
@@ -251,6 +285,7 @@ class Scenario:
251285
line_number: int
252286
steps: list[Step]
253287
tags: set[str] = field(default_factory=set)
288+
description: list[str] = field(default_factory=list)
254289

255290

256291
@dataclass

tests/feature/test_description.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ def test_description(pytester):
1919
Some description goes here.
2020
2121
Scenario: Description
22+
Also, the scenario can have a description.
23+
24+
It goes here between the scenario name
25+
and the first step.
2226
Given I have a bar
2327
"""
2428
),
@@ -39,7 +43,7 @@ def test_description():
3943
def _():
4044
return "bar"
4145
42-
def test_scenario_description():
46+
def test_feature_description():
4347
assert test_description.__scenario__.feature.description == textwrap.dedent(
4448
\"\"\"\\
4549
In order to achieve something
@@ -49,9 +53,18 @@ def test_scenario_description():
4953
5054
Some description goes here.\"\"\"
5155
)
56+
57+
def test_scenario_description():
58+
assert test_description.__scenario__.description == textwrap.dedent(
59+
\"\"\"\\
60+
Also, the scenario can have a description.
61+
62+
It goes here between the scenario name
63+
and the first step.\"\"\"
64+
)
5265
"""
5366
)
5467
)
5568

5669
result = pytester.runpytest()
57-
result.assert_outcomes(passed=2)
70+
result.assert_outcomes(passed=3)

0 commit comments

Comments
 (0)