Skip to content

Commit 2ab960a

Browse files
author
Michiel Holtkamp
committed
Fill in step names in cucumber report.
According to [1], when the outline scenarios are reported in 'expand' mode, all the outlines will be expanded to scenarios with their test results. Also, the step name parameters are filled in. This is a different mode than without 'expand' mode[2], where the scenario outlines are reported so the step names are not filled in and there are no test results. The "examples" are reported in this mode. Since pytest-bdd does report the results of each step of an outline scenario as separate scenarios, it looks more like the 'expand' mode, so I've added the filling in of the parameters in the step names to make it more compliant. [1] https://relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter#scenario-outline-expanded [2] https://relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter#scenario-outline
1 parent 11c6097 commit 2ab960a

File tree

2 files changed

+171
-3
lines changed

2 files changed

+171
-3
lines changed

pytest_bdd/cucumber_json.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import time
77

88
import py
9+
import re
910
import six
1011

1112
from .feature import force_unicode
@@ -27,12 +28,21 @@ def add_options(parser):
2728
help="create cucumber json style report file at given path.",
2829
)
2930

31+
group._addoption(
32+
"--cucumberjson-expanded",
33+
"--cucumber-json-expanded",
34+
action="store_true",
35+
dest="expand",
36+
default=False,
37+
help="expand scenario outlines into scenarios and fill in the step names",
38+
)
39+
3040

3141
def configure(config):
3242
cucumber_json_path = config.option.cucumber_json_path
3343
# prevent opening json log on slave nodes (xdist)
3444
if cucumber_json_path and not hasattr(config, "slaveinput"):
35-
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path)
45+
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path, expand=config.option.expand)
3646
config.pluginmanager.register(config._bddcucumberjson)
3747

3848

@@ -47,10 +57,11 @@ class LogBDDCucumberJSON(object):
4757

4858
"""Logging plugin for cucumber like json output."""
4959

50-
def __init__(self, logfile):
60+
def __init__(self, logfile, expand=False):
5161
logfile = os.path.expanduser(os.path.expandvars(logfile))
5262
self.logfile = os.path.normpath(os.path.abspath(logfile))
5363
self.features = {}
64+
self.expand = expand
5465

5566
def append(self, obj):
5667
self.features[-1].append(obj)
@@ -95,6 +106,23 @@ def _serialize_tags(self, item):
95106
for tag in item["tags"]
96107
]
97108

109+
def _format_name(self, name, keys, values):
110+
for param, value in zip(keys, values):
111+
name = name.replace('<{}>'.format(param), value)
112+
return name
113+
114+
def _format_step_name(self, report, step):
115+
examples = report.scenario["examples"]
116+
if len(examples) == 0:
117+
return step["name"]
118+
119+
# we take the keys from the first "examples", but in each table, the keys should
120+
# be the same anyway since all the variables need to be filled in.
121+
keys, values = examples[0]["rows"]
122+
row_index = examples[0]["row_index"]
123+
124+
return self._format_name(step["name"], keys, values[row_index])
125+
98126
def pytest_runtest_logreport(self, report):
99127
try:
100128
scenario = report.scenario
@@ -112,9 +140,17 @@ def stepmap(step):
112140
scenario['failed'] = True
113141
error_message = True
114142

143+
if self.expand:
144+
# XXX The format is already 'expanded' (scenario oultines -> scenarios),
145+
# but the step names were not filled in with parameters. To be backwards
146+
# compatible, do not fill in the step names unless explicitly asked for.
147+
step_name = self._format_step_name(report, step)
148+
else:
149+
step_name = step["name"]
150+
115151
return {
116152
"keyword": step['keyword'],
117-
"name": step['name'],
153+
"name": step_name,
118154
"line": step['line_number'],
119155
"match": {
120156
"location": "",

tests/feature/test_cucumber_json.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ def test_step_trace(testdir):
4444
Scenario: Failing
4545
Given a passing step
4646
And a failing step
47+
48+
@scenario-outline-passing-tag
49+
Scenario: Passing outline
50+
Given type <type> and value <value>
51+
52+
Examples: example1
53+
| type | value |
54+
| str | hello |
55+
| int | 42 |
56+
| float | 1.0 |
4757
"""))
4858
testdir.makepyfile(textwrap.dedent("""
4959
import pytest
@@ -61,13 +71,21 @@ def some_other_passing_step():
6171
def a_failing_step():
6272
raise Exception('Error')
6373
74+
@given('type <type> and value <value>')
75+
def type_type_and_value_value():
76+
return 'pass'
77+
6478
@scenario('test.feature', 'Passing')
6579
def test_passing():
6680
pass
6781
6882
@scenario('test.feature', 'Failing')
6983
def test_failing():
7084
pass
85+
86+
@scenario('test.feature', 'Passing outline')
87+
def test_passing_outline():
88+
pass
7189
"""))
7290
result, jsonobject = runandparse(testdir)
7391
assert result.ret
@@ -156,6 +174,84 @@ def test_failing():
156174
}
157175
],
158176
"type": "scenario"
177+
},
178+
{
179+
"description": "",
180+
"keyword": "Scenario",
181+
"tags": [
182+
{
183+
"line": 14,
184+
"name": "scenario-outline-passing-tag"
185+
}
186+
],
187+
"steps": [
188+
{
189+
"line": 16,
190+
"match": {"location": ""},
191+
"result": {
192+
"status": "passed",
193+
"duration": equals_any(int)
194+
},
195+
"keyword": "Given",
196+
"name": "type <type> and value <value>"
197+
}
198+
],
199+
"line": 15,
200+
"type": "scenario",
201+
"id": "test_passing_outline[str-hello]",
202+
"name": "Passing outline"
203+
},
204+
{
205+
"description": "",
206+
"keyword": "Scenario",
207+
"tags": [
208+
{
209+
"line": 14,
210+
"name": "scenario-outline-passing-tag"
211+
}
212+
],
213+
"steps": [
214+
{
215+
"line": 16,
216+
"match": {"location": ""},
217+
"result": {
218+
"status": "passed",
219+
"duration": equals_any(int)
220+
},
221+
"keyword": "Given",
222+
"name": "type <type> and value <value>"
223+
}
224+
],
225+
"line": 15,
226+
"type": "scenario",
227+
"id": "test_passing_outline[int-42]",
228+
"name": "Passing outline"
229+
},
230+
{
231+
"description": "",
232+
"keyword": "Scenario",
233+
"tags": [
234+
{
235+
"line": 14,
236+
"name": "scenario-outline-passing-tag"
237+
}
238+
],
239+
"steps": [
240+
{
241+
"line": 16,
242+
"match": {"location": ""},
243+
"result": {
244+
"status": "passed",
245+
"duration": equals_any(int)
246+
},
247+
"keyword": "Given",
248+
"name": "type <type> and value <value>"
249+
}
250+
],
251+
"line": 15,
252+
"type": "scenario",
253+
"id": "test_passing_outline[float-1.0]",
254+
"name": "Passing outline"
159255
}
160256
],
161257
"id": os.path.join("test_step_trace0", "test.feature"),
@@ -173,3 +269,39 @@ def test_failing():
173269
]
174270

175271
assert jsonobject == expected
272+
273+
274+
def test_step_trace_with_expand_option(testdir):
275+
"""Test step trace."""
276+
testdir.makefile('.feature', test=textwrap.dedent("""
277+
@feature-tag
278+
Feature: One scenario outline, expanded to multiple scenarios
279+
280+
@scenario-outline-passing-tag
281+
Scenario: Passing outline
282+
Given type <type> and value <value>
283+
284+
Examples: example1
285+
| type | value |
286+
| str | hello |
287+
| int | 42 |
288+
| float | 1.0 |
289+
"""))
290+
testdir.makepyfile(textwrap.dedent("""
291+
import pytest
292+
from pytest_bdd import given, scenario
293+
294+
@given('type <type> and value <value>')
295+
def type_type_and_value_value():
296+
return 'pass'
297+
298+
@scenario('test.feature', 'Passing outline')
299+
def test_passing_outline():
300+
pass
301+
"""))
302+
result, jsonobject = runandparse(testdir, '--cucumber-json-expand')
303+
assert result.ret == 0
304+
305+
assert jsonobject[0]["elements"][0]["steps"][0]["name"] == "type str and value hello"
306+
assert jsonobject[0]["elements"][1]["steps"][0]["name"] == "type int and value 42"
307+
assert jsonobject[0]["elements"][2]["steps"][0]["name"] == "type float and value 1.0"

0 commit comments

Comments
 (0)