From 971528a589fc7cc70e6c615ad8cd55a514ab1521 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 21 Aug 2021 15:42:01 +0200 Subject: [PATCH 01/61] Very bare implementation of examples subsitution using templating, instead of using parametrization. `test_outlined` should succeed. --- pytest_bdd/feature.py | 7 +- pytest_bdd/parser.py | 120 +++++++++++++++++++++++++--------- pytest_bdd/scenario.py | 47 ++++++++----- tests/feature/test_outline.py | 32 ++++----- 4 files changed, 137 insertions(+), 69 deletions(-) diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index 7ab386246..e14e69feb 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -24,16 +24,17 @@ one line. """ import os.path +import typing import glob2 -from .parser import parse_feature +from .parser import parse_feature, Feature # Global features dictionary features = {} -def get_feature(base_path, filename, encoding="utf-8"): +def get_feature(base_path, filename, encoding="utf-8") -> Feature: """Get a feature by the filename. :param str base_path: Base feature directory. @@ -55,7 +56,7 @@ def get_feature(base_path, filename, encoding="utf-8"): return feature -def get_features(paths, **kwargs): +def get_features(paths, **kwargs) -> typing.List[Feature]: """Get features for given paths. :param list paths: `list` of paths (file or dirs) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 25d54904c..2ae628192 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -2,6 +2,7 @@ import os.path import re import textwrap +import typing from collections import OrderedDict from . import types, exceptions @@ -93,7 +94,7 @@ def parse_feature(basedir, filename, encoding="utf-8"): background=None, description="", ) - scenario = None + scenario: typing.Optional[TemplatedScenario] = None mode = None prev_mode = None description = [] @@ -149,7 +150,9 @@ def parse_feature(basedir, filename, encoding="utf-8"): keyword, parsed_line = parse_line(clean_line) if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]: tags = get_tags(prev_line) - feature.scenarios[parsed_line] = scenario = Scenario(feature, parsed_line, line_number, tags=tags) + feature.scenarios[parsed_line] = scenario = TemplatedScenario( + feature=feature, name=parsed_line, line_number=line_number, tags=tags + ) elif mode == types.BACKGROUND: feature.background = Background(feature=feature, line_number=line_number) elif mode == types.EXAMPLES: @@ -199,28 +202,25 @@ class Feature: """Feature.""" def __init__(self, scenarios, filename, rel_filename, name, tags, examples, background, line_number, description): - self.scenarios = scenarios + self.scenarios: typing.Mapping[str, TemplatedScenario] = scenarios self.rel_filename = rel_filename self.filename = filename - self.name = name self.tags = tags self.examples = examples self.name = name self.line_number = line_number - self.tags = tags - self.scenarios = scenarios self.description = description self.background = background -class Scenario: +class TemplatedScenario: + """A templated scenario. - """Scenario.""" + Created when parsing the feature file, it will then be combined with the examples to create a Scenario.""" - def __init__(self, feature, name, line_number, example_converters=None, tags=None): - """Scenario constructor. + def __init__(self, feature: Feature, name: str, line_number: int, tags=None): + """ - :param pytest_bdd.parser.Feature feature: Feature. :param str name: Scenario name. :param int line_number: Scenario line number. :param dict example_converters: Example table parameter converters. @@ -228,13 +228,10 @@ def __init__(self, feature, name, line_number, example_converters=None, tags=Non """ self.feature = feature self.name = name - self._steps = [] + self._steps: typing.List[Step] = [] self.examples = Examples() self.line_number = line_number - self.example_converters = example_converters self.tags = tags or set() - self.failed = False - self.test_function = None def add_step(self, step): """Add step to the scenario. @@ -244,26 +241,80 @@ def add_step(self, step): step.scenario = self self._steps.append(step) - @property - def steps(self): - """Get scenario steps including background steps. + def get_params(self, builtin=False): + """Get converted example params.""" + for examples in [self.feature.examples, self.examples]: + yield examples.get_params(None, builtin=builtin) + + def render(self, context) -> "Scenario": + steps = [ + Step( + name=templated_step.render(context), + type=templated_step.type, + indent=templated_step.indent, + line_number=templated_step.line_number, + keyword=templated_step.keyword, + ) + for templated_step in self._steps + ] + return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags) - :return: List of steps. - """ - result = [] - if self.feature.background: - result.extend(self.feature.background.steps) - result.extend(self._steps) - return result + def validate(self): + # TODO: validate examples? or nothing? + pass - @property - def params(self): - """Get parameter names. - :return: Parameter names. - :rtype: frozenset +class Scenario: + + """Scenario.""" + + def __init__(self, feature: Feature, name: str, line_number: int, steps: "typing.List[Step]", tags=None): + """Scenario constructor. + + :param pytest_bdd.parser.Feature feature: Feature. + :param str name: Scenario name. + :param int line_number: Scenario line number. + :param dict example_converters: Example table parameter converters. + :param set tags: Set of tags. """ - return frozenset(sum((list(step.params) for step in self.steps), [])) + self.feature = feature + self.name = name + self.steps = steps + self.examples = Examples() + self.line_number = line_number + self.tags = tags or set() + self.failed = False + self.test_function = None + + # + # def add_step(self, step): + # """Add step to the scenario. + # + # :param pytest_bdd.parser.Step step: Step. + # """ + # step.scenario = self + # self._steps.append(step) + + # @property + # def steps(self): + # """Get scenario steps including background steps. + # + # :return: List of steps. + # """ + # result = [] + # if self.feature.background: + # result.extend(self.feature.background.steps) + # result.extend(self._steps) + # return result + + # @property + # def params(self): + # """Get parameter names. + # + # :return: Parameter names. + # :rtype: frozenset + # """ + # return frozenset(sum((list(step.params) for step in self.steps), [])) def get_example_params(self): """Get example parameter names.""" @@ -352,6 +403,13 @@ def params(self): """Get step params.""" return tuple(frozenset(STEP_PARAM_RE.findall(self.name))) + def render(self, context: typing.Mapping[str, typing.Any]): + def replacer(m: typing.Match): + varname = m.group(1) + return str(context[varname]) + + return STEP_PARAM_RE.sub(replacer, self.name) + class Background: diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 5a8729d53..2cc8cbc83 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -13,6 +13,7 @@ import collections import os import re +import typing import pytest from _pytest.fixtures import FixtureLookupError @@ -22,6 +23,9 @@ from .steps import get_step_fixture_name, inject_fixture from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path +if typing.TYPE_CHECKING: + from .parser import TemplatedScenario, Scenario, Feature + PYTHON_REPLACE_REGEX = re.compile(r"\W") ALPHA_REGEX = re.compile(r"^\d+_*") @@ -113,7 +117,7 @@ def _execute_step_function(request, scenario, step, step_func): raise -def _execute_scenario(feature, scenario, request): +def _execute_scenario(feature: "Feature", scenario: "Scenario", request): """Execute the scenario. :param feature: Feature. @@ -141,7 +145,9 @@ def _execute_scenario(feature, scenario, request): FakeRequest = collections.namedtuple("FakeRequest", ["module"]) -def _get_scenario_decorator(feature, feature_name, scenario, scenario_name): +def _get_scenario_decorator( + feature: "Feature", feature_name: str, templated_scenario: "TemplatedScenario", scenario_name: str +): # HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception # when the decorator is misused. # Pytest inspect the signature to determine the required fixtures, and in that case it would look @@ -155,32 +161,39 @@ def decorator(*args): ) [fn] = args args = get_args(fn) - function_args = list(args) - for arg in scenario.get_example_params(): - if arg not in function_args: - function_args.append(arg) - @pytest.mark.usefixtures(*function_args) - def scenario_wrapper(request): + contexts = [] + for examples in templated_scenario.get_params(): + if not examples: + continue + header, rows = examples + for row in rows: + assert len(header) == len(row) + + context = dict(zip(header, row)) + contexts.append(context) + + @pytest.mark.parametrize("_pytest_bdd_example", contexts) + def scenario_wrapper(request, _pytest_bdd_example): + scenario = templated_scenario.render(_pytest_bdd_example) _execute_scenario(feature, scenario, request) return fn(*(request.getfixturevalue(arg) for arg in args)) - for param_set in scenario.get_params(): - if param_set: - scenario_wrapper = pytest.mark.parametrize(*param_set)(scenario_wrapper) - for tag in scenario.tags.union(feature.tags): + for tag in templated_scenario.tags.union(feature.tags): config = CONFIG_STACK[-1] config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper) scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}" - scenario_wrapper.__scenario__ = scenario - scenario.test_function = scenario_wrapper + scenario_wrapper.__scenario__ = templated_scenario + scenario.test_function = scenario_wrapper # TODO: Check usages and remove return scenario_wrapper return decorator -def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=None, features_base_dir=None): +def scenario( + feature_name: str, scenario_name: str, encoding: str = "utf-8", example_converters=None, features_base_dir=None +): """Scenario decorator. :param str feature_name: Feature file name. Absolute or relative to the configured feature base path. @@ -207,13 +220,13 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N f'Scenario "{scenario_name}" in feature "{feature_name}" in {feature.filename} is not found.' ) - scenario.example_converters = example_converters + # scenario.example_converters = example_converters # Validate the scenario scenario.validate() return _get_scenario_decorator( - feature=feature, feature_name=feature_name, scenario=scenario, scenario_name=scenario_name + feature=feature, feature_name=feature_name, templated_scenario=scenario, scenario_name=scenario_name ) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 92a1d6d58..4de6a22d4 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -4,27 +4,28 @@ from tests.utils import assert_outcomes STEPS = """\ -from pytest_bdd import given, when, then +from pytest_bdd import parsers, given, when, then -@given("there are cucumbers", target_fixture="start_cucumbers") +@given(parsers.parse("there are {start} cucumbers"), target_fixture="start_cucumbers") def start_cucumbers(start): - assert isinstance(start, int) + assert isinstance(start, str) + start = int(start) return dict(start=start) -@when("I eat cucumbers") +@when(parsers.parse("I eat {eat} cucumbers")) def eat_cucumbers(start_cucumbers, eat): - assert isinstance(eat, float) + assert isinstance(eat, str) + eat = int(eat) start_cucumbers["eat"] = eat -@then("I should have cucumbers") -def should_have_left_cucumbers(start_cucumbers, start, eat, left): +@then(parsers.parse("I should have {left} cucumbers")) +def should_have_left_cucumbers(start_cucumbers, left): assert isinstance(left, str) - assert start - eat == int(left) - assert start_cucumbers["start"] == start - assert start_cucumbers["eat"] == eat + left = int(left) + assert left == start_cucumbers["start"] - start_cucumbers["eat"] """ @@ -44,6 +45,7 @@ def test_outlined(testdir): | start | eat | left | | 12 | 5 | 7 | # a comment | 5 | 4 | 1 | + | 5 | 4 | 43 | # Control case. This should fail. """ ), @@ -63,19 +65,13 @@ def test_outlined(testdir): example_converters=dict(start=int, eat=float, left=str) ) def test_outline(request): - assert get_parametrize_markers_args(request.node) == ( - ["start", "eat", "left"], - [ - [12, 5.0, "7"], - [5, 4.0, "1"], - ], - ) + pass """ ) ) result = testdir.runpytest() - result.assert_outcomes(passed=2) + result.assert_outcomes(passed=2, failed=1) def test_wrongly_outlined(testdir): From 8c0db0fd6cbb0a5e28ce409e79fff6f681d3e223 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 10:40:21 +0200 Subject: [PATCH 02/61] Start removing example_converters --- pytest_bdd/parser.py | 2 -- pytest_bdd/scenario.py | 6 +----- tests/feature/test_cucumber_json.py | 10 +++++----- tests/feature/test_outline.py | 1 - 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 2ae628192..b1d65bc54 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -223,7 +223,6 @@ def __init__(self, feature: Feature, name: str, line_number: int, tags=None): :param str name: Scenario name. :param int line_number: Scenario line number. - :param dict example_converters: Example table parameter converters. :param set tags: Set of tags. """ self.feature = feature @@ -274,7 +273,6 @@ def __init__(self, feature: Feature, name: str, line_number: int, steps: "typing :param pytest_bdd.parser.Feature feature: Feature. :param str name: Scenario name. :param int line_number: Scenario line number. - :param dict example_converters: Example table parameter converters. :param set tags: Set of tags. """ self.feature = feature diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 2cc8cbc83..68d0bfcfa 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -191,16 +191,12 @@ def scenario_wrapper(request, _pytest_bdd_example): return decorator -def scenario( - feature_name: str, scenario_name: str, encoding: str = "utf-8", example_converters=None, features_base_dir=None -): +def scenario(feature_name: str, scenario_name: str, 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 str encoding: Feature file encoding. - :param dict example_converters: optional `dict` of example converter function, where key is the name of the - example parameter, and value is the converter function. """ scenario_name = str(scenario_name) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index d7db5f0e3..3577f2ac1 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -262,9 +262,9 @@ def test_step_trace_with_expand_option(testdir): textwrap.dedent( """ import pytest - from pytest_bdd import given, scenario + from pytest_bdd import given, scenario, parsers - @given('type and value ') + @given(parsers.parse('type {type} and value {value}')) def type_type_and_value_value(): return 'pass' @@ -282,6 +282,7 @@ def test_passing_outline(): assert jsonobject[0]["elements"][2]["steps"][0]["name"] == "type float and value 1.0" +# TODO: This test seems irrelevant now, remove it? def test_converters_dict_with_expand_option(testdir): """Test that `--cucumber-json-expanded` works correctly when using `example_converters`.""" testdir.makefile( @@ -302,16 +303,15 @@ def test_converters_dict_with_expand_option(testdir): textwrap.dedent( """ import pytest - from pytest_bdd import given, scenario + from pytest_bdd import given, scenario, parsers - @given('there is an intvalue and stringvalue and floatvalue ') + @given(parsers.parse('there is an intvalue {intvalue} and stringvalue {stringvalue} and floatvalue {floatvalue}')) def type_type_and_value_value(): pass @scenario( 'test.feature', 'Passing outline', - example_converters={"intvalue":int, "stringvalue":str, "floatvalue":float}, ) def test_passing_outline(): pass diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 4de6a22d4..b3b06be31 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -62,7 +62,6 @@ def test_outlined(testdir): @scenario( "outline.feature", "Outlined given, when, thens", - example_converters=dict(start=int, eat=float, left=str) ) def test_outline(request): pass From 54ac82115f07b84b0a2f99b13cd199b67f6438ac Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 10:41:46 +0200 Subject: [PATCH 03/61] Make sure that `_pytest_bdd_example` is always not empty, otherwise tests will be skipped --- pytest_bdd/scenario.py | 5 ++++- tests/feature/test_cucumber_json.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 68d0bfcfa..b79c34e40 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -171,7 +171,10 @@ def decorator(*args): assert len(header) == len(row) context = dict(zip(header, row)) - contexts.append(context) + contexts.append(pytest.param(context, id="-".join(context.values()))) + + if not contexts: + contexts = [pytest.param({}, id="")] @pytest.mark.parametrize("_pytest_bdd_example", contexts) def scenario_wrapper(request, _pytest_bdd_example): diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 3577f2ac1..f3bbac203 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -104,6 +104,8 @@ def test_passing_outline(): ) ) result, jsonobject = runandparse(testdir) + result.assert_outcomes(passed=4, failed=1) + assert result.ret expected = [ { From 5c84d052d8d170b20dd7a44e38d24bb9b84fd354 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 10:42:40 +0200 Subject: [PATCH 04/61] Fix test --- tests/feature/test_cucumber_json.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index f3bbac203..0f161cc94 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -71,7 +71,7 @@ def test_step_trace(testdir): textwrap.dedent( """ import pytest - from pytest_bdd import given, when, scenario + from pytest_bdd import given, when, scenario, parsers @given('a passing step') def a_passing_step(): @@ -85,7 +85,7 @@ def some_other_passing_step(): def a_failing_step(): raise Exception('Error') - @given('type and value ') + @given(parsers.parse('type {type} and value {value}')) def type_type_and_value_value(): return 'pass' @@ -113,7 +113,7 @@ def test_passing_outline(): "elements": [ { "description": "", - "id": "test_passing", + "id": "test_passing[]", "keyword": "Scenario", "line": 5, "name": "Passing", @@ -138,7 +138,7 @@ def test_passing_outline(): }, { "description": "", - "id": "test_failing", + "id": "test_failing[]", "keyword": "Scenario", "line": 10, "name": "Failing", @@ -171,7 +171,7 @@ def test_passing_outline(): "match": {"location": ""}, "result": {"status": "passed", "duration": OfType(int)}, "keyword": "Given", - "name": "type and value ", + "name": "type str and value hello", } ], "line": 15, @@ -189,7 +189,7 @@ def test_passing_outline(): "match": {"location": ""}, "result": {"status": "passed", "duration": OfType(int)}, "keyword": "Given", - "name": "type and value ", + "name": "type int and value 42", } ], "line": 15, @@ -207,7 +207,7 @@ def test_passing_outline(): "match": {"location": ""}, "result": {"status": "passed", "duration": OfType(int)}, "keyword": "Given", - "name": "type and value ", + "name": "type float and value 1.0", } ], "line": 15, @@ -228,6 +228,7 @@ def test_passing_outline(): assert jsonobject == expected +# TODO: This test is irrelevant too, since the "expanded" option is the only option now def test_step_trace_with_expand_option(testdir): """Test step trace.""" testdir.makefile( From 58bc825a08980257b1fbe932a87be631d3b3d44e Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 10:51:00 +0200 Subject: [PATCH 05/61] Remove occurrences of example_converters --- CHANGES.rst | 1 + pytest_bdd/parser.py | 3 +- pytest_bdd/scenario.py | 2 -- tests/feature/test_cucumber_json.py | 43 ----------------------------- tests/feature/test_outline.py | 3 -- tests/feature/test_report.py | 4 +-- 6 files changed, 5 insertions(+), 51 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1ac34e92a..ce575309a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,7 @@ Changelog Unreleased ----------- +- Removed ``example_converters`` from ``scenario(...)`` signature 4.1.0 ----------- diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index b1d65bc54..8e6c62f25 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -321,7 +321,7 @@ def get_example_params(self): def get_params(self, builtin=False): """Get converted example params.""" for examples in [self.feature.examples, self.examples]: - yield examples.get_params(self.example_converters, builtin=builtin) + yield examples.get_params(None, builtin=builtin) def validate(self): """Validate the scenario. @@ -468,6 +468,7 @@ def add_example_row(self, param, values): self.example_params.append(param) self.vertical_examples.append(values) + # TODO: Remove `converters` def get_params(self, converters, builtin=False): """Get scenario pytest parametrization table. diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index b79c34e40..24f4607a0 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -219,8 +219,6 @@ def scenario(feature_name: str, scenario_name: str, encoding: str = "utf-8", fea f'Scenario "{scenario_name}" in feature "{feature_name}" in {feature.filename} is not found.' ) - # scenario.example_converters = example_converters - # Validate the scenario scenario.validate() diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 0f161cc94..a8836a6a6 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -283,46 +283,3 @@ def test_passing_outline(): assert jsonobject[0]["elements"][0]["steps"][0]["name"] == "type str and value hello" assert jsonobject[0]["elements"][1]["steps"][0]["name"] == "type int and value 42" assert jsonobject[0]["elements"][2]["steps"][0]["name"] == "type float and value 1.0" - - -# TODO: This test seems irrelevant now, remove it? -def test_converters_dict_with_expand_option(testdir): - """Test that `--cucumber-json-expanded` works correctly when using `example_converters`.""" - testdir.makefile( - ".feature", - test=textwrap.dedent( - """ - Feature: Expanded option with example converters - Scenario: Passing outline - Given there is an intvalue and stringvalue and floatvalue - - Examples: example1 - | intvalue | stringvalue | floatvalue | - | 1 | hello | 1.0 | - """ - ), - ) - testdir.makepyfile( - textwrap.dedent( - """ - import pytest - from pytest_bdd import given, scenario, parsers - - @given(parsers.parse('there is an intvalue {intvalue} and stringvalue {stringvalue} and floatvalue {floatvalue}')) - def type_type_and_value_value(): - pass - - @scenario( - 'test.feature', - 'Passing outline', - ) - def test_passing_outline(): - pass - """ - ) - ) - result, jsonobject = runandparse(testdir, "--cucumber-json-expanded") - assert result.ret == 0 - - expanded_step_name = jsonobject[0]["elements"][0]["steps"][0]["name"] - assert expanded_step_name == "there is an intvalue 1 and stringvalue hello and floatvalue 1.0" diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index b3b06be31..12d89ec73 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -234,7 +234,6 @@ def other_fixture(request): @scenario( "outline.feature", "Outlined given, when, thens", - example_converters=dict(start=int, eat=float, left=str) ) def test_outline(other_fixture): pass @@ -278,7 +277,6 @@ def test_vertical_example(testdir): @scenario( "outline.feature", "Outlined with vertical example table", - example_converters=dict(start=int, eat=float, left=str) ) def test_outline(request): assert get_parametrize_markers_args(request.node) == ( @@ -330,7 +328,6 @@ def test_outlined_feature(testdir): @scenario( "outline.feature", "Outlined given, when, thens", - example_converters=dict(start=int, eat=float, left=str) ) def test_outline(request): assert get_parametrize_markers_args(request.node) == ( diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 720611032..6bc86e542 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -95,7 +95,7 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): assert start_cucumbers['start'] == start assert start_cucumbers['eat'] == eat - scenarios('test.feature', example_converters=dict(start=int, eat=float, left=str)) + scenarios('test.feature') """ ) ) @@ -325,7 +325,7 @@ def point(point): @pytest.mark.parametrize('alien', [Alien()]) - @scenario('test.feature', 'Complex', example_converters=dict(point=Point.parse)) + @scenario('test.feature', 'Complex') def test_complex(alien): pass From 34e3da12fb3a10fe358e509359d6dfeae1d1c2d0 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 10:57:18 +0200 Subject: [PATCH 06/61] Fix background steps not being used --- pytest_bdd/parser.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 8e6c62f25..6bd9be30c 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -246,6 +246,10 @@ def get_params(self, builtin=False): yield examples.get_params(None, builtin=builtin) def render(self, context) -> "Scenario": + background = self.feature.background + + templated_steps = (background.steps if background else []) + self._steps + steps = [ Step( name=templated_step.render(context), @@ -254,7 +258,7 @@ def render(self, context) -> "Scenario": line_number=templated_step.line_number, keyword=templated_step.keyword, ) - for templated_step in self._steps + for templated_step in templated_steps ] return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags) From aa2ef257b86f1551edac274300ab6113a509ce2a Mon Sep 17 00:00:00 2001 From: Oleg Pidsadnyi Date: Sun, 22 Aug 2021 15:59:12 +0200 Subject: [PATCH 07/61] readme updated --- README.rst | 46 +++++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/README.rst b/README.rst index 5329a64fa..724c84006 100644 --- a/README.rst +++ b/README.rst @@ -112,7 +112,6 @@ Scenario decorator The scenario decorator can accept the following optional keyword arguments: * ``encoding`` - decode content of feature file in specific encoding. UTF-8 is default. -* ``example_converters`` - mapping to pass functions to convert example values provided in feature files. Functions decorated with the `scenario` decorator behave like a normal test function, and they will be executed after all scenario steps. @@ -555,25 +554,24 @@ The code will look like: @scenario( "outline.feature", "Outlined given, when, thens", - example_converters=dict(start=int, eat=float, left=str) ) def test_outlined(): pass - @given("there are cucumbers", target_fixture="start_cucumbers") + @given(parsers.parse("there are {start} cucumbers", target_fixture="start_cucumbers")) def start_cucumbers(start): assert isinstance(start, int) return dict(start=start) - @when("I eat cucumbers") + @when(parsers.parse("I eat {eat} cucumbers")) def eat_cucumbers(start_cucumbers, eat): assert isinstance(eat, float) start_cucumbers["eat"] = eat - @then("I should have cucumbers") + @then(parsers.parse("I should have {left} cucumbers")) def should_have_left_cucumbers(start_cucumbers, start, eat, left): assert isinstance(left, str) assert start - eat == int(left) @@ -672,17 +670,17 @@ The code will look like: """We don't need to do anything here, everything will be managed by the scenario decorator.""" - @given("there are cucumbers", target_fixture="start_cucumbers") + @given(parsers.parse("there are {start} cucumbers"), target_fixture="start_cucumbers") def start_cucumbers(start): return dict(start=start) - @when("I eat cucumbers") + @when(parsers.parse("I eat {eat} cucumbers")) def eat_cucumbers(start_cucumbers, start, eat): start_cucumbers["eat"] = eat - @then("I should have cucumbers") + @then(parsers.parse("I should have {left} cucumbers")) def should_have_left_cucumbers(start_cucumbers, start, eat, left): assert start - eat == left assert start_cucumbers["start"] == start @@ -1208,37 +1206,27 @@ As as side effect, the tool will validate the files for format errors, also some ordering of the types of the steps. -.. _Migration from 3.x.x: +.. _Migration from 4.x.x: -Migration of your tests from versions 3.x.x +Migration of your tests from versions 4.x.x ------------------------------------------- - -Given steps are no longer fixtures. In case it is needed to make given step setup a fixture -the target_fixture parameter should be used. - +The templated steps should use step argument parsers in order to match the scenario outlines +and get the values from the example tables. .. code-block:: python - @given("there's an article", target_fixture="article") - def there_is_an_article(): - return Article() - + # Instead of + # @given("there are in the box") + # def given_cucumbers(cucumbers): + # pass -Given steps no longer have fixture parameter. In fact the step may depend on multiple fixtures. -Just normal step declaration with the dependency injection should be used. - -.. code-block:: python - - @given("there's an article") - def there_is_an_article(article): + @given(parsers.parse("there are {cucumbers} in the box")) + def given_cucumbers(cucumbers): pass -Strict gherkin option is removed, so the ``strict_gherkin`` parameter can be removed from the scenario decorators -as well as ``bdd_strict_gherkin`` from the ini files. - -Step validation handlers for the hook ``pytest_bdd_step_validation_error`` should be removed. +Scenario `example_converters` are removed in favor of the converters provided on the step level. License From 273c594b8bea118985ac9fed3edc07b8ce8fed27 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 16:24:46 +0200 Subject: [PATCH 08/61] Fix test --- tests/feature/test_gherkin_terminal_reporter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py index dde1ad67e..7123b9ba6 100644 --- a/tests/feature/test_gherkin_terminal_reporter.py +++ b/tests/feature/test_gherkin_terminal_reporter.py @@ -204,18 +204,18 @@ def test_step_parameters_should_be_replaced_by_their_values(testdir): testdir.makepyfile( test_gherkin=textwrap.dedent( """\ - from pytest_bdd import given, when, scenario, then + from pytest_bdd import given, when, scenario, then, parsers - @given('there are cucumbers', target_fixture="start_cucumbers") + @given(parsers.parse('there are {start} cucumbers'), target_fixture="start_cucumbers") def start_cucumbers(start): return start - @when('I eat cucumbers') + @when(parsers.parse('I eat {eat} cucumbers')) def eat_cucumbers(start_cucumbers, eat): pass - @then('I should have cucumbers') - def should_have_left_cucumbers(start_cucumbers, start, eat, left): + @then(parsers.parse('I should have {left} cucumbers')) + def should_have_left_cucumbers(start_cucumbers, left): pass @scenario('test.feature', 'Scenario example 2') From 13ce955b1689334770a7a65c95915b480c16a114 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 17:22:31 +0200 Subject: [PATCH 09/61] Cleanup code --- pytest_bdd/cucumber_json.py | 31 ++-------- pytest_bdd/gherkin_terminal_reporter.py | 17 +----- pytest_bdd/parser.py | 75 ++++++------------------- pytest_bdd/reporting.py | 27 --------- pytest_bdd/scenario.py | 3 +- tests/feature/test_report.py | 52 +++++------------ 6 files changed, 38 insertions(+), 167 deletions(-) diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index 1a9458e72..c5ee984c3 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -19,6 +19,7 @@ def add_options(parser): help="create cucumber json style report file at given path.", ) + # TODO: Remove this group._addoption( "--cucumberjson-expanded", "--cucumber-json-expanded", @@ -33,7 +34,7 @@ def configure(config): cucumber_json_path = config.option.cucumber_json_path # prevent opening json log on worker nodes (xdist) if cucumber_json_path and not hasattr(config, "workerinput"): - config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path, expand=config.option.expand) + config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path) config.pluginmanager.register(config._bddcucumberjson) @@ -48,11 +49,10 @@ class LogBDDCucumberJSON: """Logging plugin for cucumber like json output.""" - def __init__(self, logfile, expand=False): + def __init__(self, logfile): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) self.features = {} - self.expand = expand def append(self, obj): self.features[-1].append(obj) @@ -88,23 +88,6 @@ def _serialize_tags(self, item): """ return [{"name": tag, "line": item["line_number"] - 1} for tag in item["tags"]] - def _format_name(self, name, keys, values): - for param, value in zip(keys, values): - name = name.replace(f"<{param}>", str(value)) - return name - - def _format_step_name(self, report, step): - examples = report.scenario["examples"] - if len(examples) == 0: - return step["name"] - - # we take the keys from the first "examples", but in each table, the keys should - # be the same anyway since all the variables need to be filled in. - keys, values = examples[0]["rows"] - row_index = examples[0]["row_index"] - - return self._format_name(step["name"], keys, values[row_index]) - def pytest_runtest_logreport(self, report): try: scenario = report.scenario @@ -122,13 +105,7 @@ def stepmap(step): scenario["failed"] = True error_message = True - if self.expand: - # XXX The format is already 'expanded' (scenario oultines -> scenarios), - # but the step names were not filled in with parameters. To be backwards - # compatible, do not fill in the step names unless explicitly asked for. - step_name = self._format_step_name(report, step) - else: - step_name = step["name"] + step_name = step["name"] return { "keyword": step["keyword"], diff --git a/pytest_bdd/gherkin_terminal_reporter.py b/pytest_bdd/gherkin_terminal_reporter.py index 97f7ae797..094bc951f 100644 --- a/pytest_bdd/gherkin_terminal_reporter.py +++ b/pytest_bdd/gherkin_terminal_reporter.py @@ -93,24 +93,9 @@ def pytest_runtest_logreport(self, report): self._tw.write(report.scenario["name"], **scenario_markup) self._tw.write("\n") for step in report.scenario["steps"]: - if self.config.option.expand: - step_name = self._format_step_name(step["name"], **report.scenario["example_kwargs"]) - else: - step_name = step["name"] - self._tw.write(" {} {}\n".format(step["keyword"], step_name), **scenario_markup) + self._tw.write(" {} {}\n".format(step["keyword"], step["name"]), **scenario_markup) self._tw.write(" " + word, **word_markup) self._tw.write("\n\n") else: return TerminalReporter.pytest_runtest_logreport(self, rep) self.stats.setdefault(cat, []).append(rep) - - def _format_step_name(self, step_name, **example_kwargs): - while True: - param_match = re.search(STEP_PARAM_RE, step_name) - if not param_match: - break - param_token = param_match.group(0) - param_name = param_match.group(1) - param_value = example_kwargs[param_name] - step_name = step_name.replace(param_token, param_value) - return step_name diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 6bd9be30c..713262426 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -245,6 +245,11 @@ def get_params(self, builtin=False): for examples in [self.feature.examples, self.examples]: yield examples.get_params(None, builtin=builtin) + @property + def steps(self): + background = self.feature.background + return (background.steps if background else []) + self._steps + def render(self, context) -> "Scenario": background = self.feature.background @@ -263,8 +268,19 @@ def render(self, context) -> "Scenario": return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags) def validate(self): - # TODO: validate examples? or nothing? - pass + """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 != example_params: + raise exceptions.ScenarioExamplesNotValidError( + """Scenario "{}" in the feature "{}" has not valid examples. """ + """Set of step parameters {} should match set of example values {}.""".format( + self.name, self.feature.filename, sorted(params), sorted(example_params) + ) + ) class Scenario: @@ -282,66 +298,11 @@ def __init__(self, feature: Feature, name: str, line_number: int, steps: "typing self.feature = feature self.name = name self.steps = steps - self.examples = Examples() self.line_number = line_number self.tags = tags or set() self.failed = False self.test_function = None - # - # def add_step(self, step): - # """Add step to the scenario. - # - # :param pytest_bdd.parser.Step step: Step. - # """ - # step.scenario = self - # self._steps.append(step) - - # @property - # def steps(self): - # """Get scenario steps including background steps. - # - # :return: List of steps. - # """ - # result = [] - # if self.feature.background: - # result.extend(self.feature.background.steps) - # result.extend(self._steps) - # return result - - # @property - # def params(self): - # """Get parameter names. - # - # :return: Parameter names. - # :rtype: frozenset - # """ - # return frozenset(sum((list(step.params) for step in self.steps), [])) - - def get_example_params(self): - """Get example parameter names.""" - return set(self.examples.example_params + self.feature.examples.example_params) - - def get_params(self, builtin=False): - """Get converted example params.""" - for examples in [self.feature.examples, self.examples]: - yield examples.get_params(None, builtin=builtin) - - def validate(self): - """Validate the scenario. - - :raises ScenarioValidationError: when scenario is not valid - """ - params = self.params - example_params = self.get_example_params() - if params and example_params and params != example_params: - raise exceptions.ScenarioExamplesNotValidError( - """Scenario "{}" in the feature "{}" has not valid examples. """ - """Set of step parameters {} should match set of example values {}.""".format( - self.name, self.feature.filename, sorted(params), sorted(example_params) - ) - ) - class Step: diff --git a/pytest_bdd/reporting.py b/pytest_bdd/reporting.py index e9925708c..d4e9e81a5 100644 --- a/pytest_bdd/reporting.py +++ b/pytest_bdd/reporting.py @@ -70,21 +70,6 @@ def __init__(self, scenario, node): """ self.scenario = scenario self.step_reports = [] - self.param_index = None - parametrize_args = get_parametrize_markers_args(node) - if parametrize_args and scenario.examples: - param_names = ( - parametrize_args[0] if isinstance(parametrize_args[0], (tuple, list)) else [parametrize_args[0]] - ) - param_values = parametrize_args[1] - node_param_values = [node.funcargs[param_name] for param_name in param_names] - if node_param_values in param_values: - self.param_index = param_values.index(node_param_values) - elif tuple(node_param_values) in param_values: - self.param_index = param_values.index(tuple(node_param_values)) - self.example_kwargs = { - example_param: str(node.funcargs[example_param]) for example_param in scenario.get_example_params() - } @property def current_step_report(self): @@ -112,7 +97,6 @@ def serialize(self): scenario = self.scenario feature = scenario.feature - params = sum(scenario.get_params(builtin=True), []) if scenario.examples else None return { "steps": [step_report.serialize() for step_report in self.step_reports], "name": scenario.name, @@ -126,17 +110,6 @@ def serialize(self): "description": feature.description, "tags": sorted(feature.tags), }, - "examples": [ - { - "name": scenario.examples.name, - "line_number": scenario.examples.line_number, - "rows": params, - "row_index": self.param_index, - } - ] - if scenario.examples - else [], - "example_kwargs": self.example_kwargs, } def fail(self): diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 24f4607a0..63d7f2d28 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -180,7 +180,8 @@ def decorator(*args): def scenario_wrapper(request, _pytest_bdd_example): scenario = templated_scenario.render(_pytest_bdd_example) _execute_scenario(feature, scenario, request) - return fn(*(request.getfixturevalue(arg) for arg in args)) + fixture_values = [request.getfixturevalue(arg) for arg in args] + return fn(*fixture_values) for tag in templated_scenario.tags.union(feature.tags): config = CONFIG_STACK[-1] diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 6bc86e542..1d250391e 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -62,7 +62,7 @@ def test_step_trace(testdir): textwrap.dedent( """ import pytest - from pytest_bdd import given, when, then, scenarios + from pytest_bdd import given, when, then, scenarios, parsers @given('a passing step') def a_passing_step(): @@ -76,24 +76,20 @@ def some_other_passing_step(): def a_failing_step(): raise Exception('Error') - @given('there are cucumbers', target_fixture="start_cucumbers") + @given(parsers.parse('there are {start} cucumbers'), target_fixture="start_cucumbers") def start_cucumbers(start): - assert isinstance(start, int) - return dict(start=start) + return dict(start=int(start)) - @when('I eat cucumbers') + @when(parsers.parse('I eat {eat} cucumbers')) def eat_cucumbers(start_cucumbers, eat): - assert isinstance(eat, float) - start_cucumbers['eat'] = eat + start_cucumbers['eat'] = float(eat) - @then('I should have cucumbers') - def should_have_left_cucumbers(start_cucumbers, start, eat, left): - assert isinstance(left, str) - assert start - eat == int(left) - assert start_cucumbers['start'] == start - assert start_cucumbers['eat'] == eat + @then(parsers.parse('I should have {left} cucumbers')) + def should_have_left_cucumbers(start_cucumbers, left): + assert start_cucumbers['start'] - start_cucumbers['eat'] == int(left) + scenarios('test.feature') """ @@ -101,7 +97,7 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): ) result = testdir.inline_run("-vvl") assert result.ret - report = result.matchreport("test_passing", when="call").scenario + report = result.matchreport("test_passing[]", when="call").scenario expected = { "feature": { "description": "", @@ -132,13 +128,11 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): }, ], "tags": ["scenario-passing-tag"], - "examples": [], - "example_kwargs": {}, } assert report == expected - report = result.matchreport("test_failing", when="call").scenario + report = result.matchreport("test_failing[]", when="call").scenario expected = { "feature": { "description": "", @@ -169,12 +163,10 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): }, ], "tags": ["scenario-failing-tag"], - "examples": [], - "example_kwargs": {}, } assert report == expected - report = result.matchreport("test_outlined[12-5.0-7]", when="call").scenario + report = result.matchreport("test_outlined[12-5-7]", when="call").scenario expected = { "feature": { "description": "", @@ -213,19 +205,10 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): }, ], "tags": [], - "examples": [ - { - "line_number": 19, - "name": None, - "row_index": 0, - "rows": [["start", "eat", "left"], [[12, 5.0, "7"], [5, 4.0, "1"]]], - } - ], - "example_kwargs": {"eat": "5.0", "left": "7", "start": "12"}, } assert report == expected - report = result.matchreport("test_outlined[5-4.0-1]", when="call").scenario + report = result.matchreport("test_outlined[5-4-1]", when="call").scenario expected = { "feature": { "description": "", @@ -264,15 +247,6 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): }, ], "tags": [], - "examples": [ - { - "line_number": 19, - "name": None, - "row_index": 1, - "rows": [["start", "eat", "left"], [[12, 5.0, "7"], [5, 4.0, "1"]]], - } - ], - "example_kwargs": {"eat": "4.0", "left": "1", "start": "5"}, } assert report == expected From 464ef40a7cbf10aede89281cc43e0382bac16596 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 17:30:48 +0200 Subject: [PATCH 10/61] Fix indirect fixtures not working --- pytest_bdd/scenario.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 63d7f2d28..4e600c9e5 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -176,7 +176,10 @@ def decorator(*args): if not contexts: contexts = [pytest.param({}, id="")] - @pytest.mark.parametrize("_pytest_bdd_example", contexts) + # We need to tell pytest that the original function requires its fixtures, + # otherwise indirect fixtures would not work. + @pytest.mark.usefixtures(*args) + @pytest.mark.parametrize("_pytest_bdd_example", contexts) # Parametrize the scenario outlines def scenario_wrapper(request, _pytest_bdd_example): scenario = templated_scenario.render(_pytest_bdd_example) _execute_scenario(feature, scenario, request) From af6e4a2f0f9d6c902a24494d1e41b8848439e43e Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 17:39:50 +0200 Subject: [PATCH 11/61] Fix test --- tests/feature/test_no_sctrict_gherkin.py | 99 ++++++++++++++---------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/tests/feature/test_no_sctrict_gherkin.py b/tests/feature/test_no_sctrict_gherkin.py index f4918940b..5e8d4ebd9 100644 --- a/tests/feature/test_no_sctrict_gherkin.py +++ b/tests/feature/test_no_sctrict_gherkin.py @@ -3,49 +3,64 @@ def test_background_no_strict_gherkin(testdir): """Test background no strict gherkin.""" - prepare_test_dir(testdir) + testdir.makepyfile( + test_gherkin=""" + import pytest - testdir.makefile( - ".feature", - no_strict_gherkin_background=""" - Feature: No strict Gherkin Background support + from pytest_bdd import ( + when, + scenario, + ) - Background: - When foo has a value "bar" - And foo is not boolean - And foo has not a value "baz" + @scenario( + "no_strict_gherkin_background.feature", + "Test background", + ) + def test_background(): + pass - Scenario: Test background - """, - ) + @pytest.fixture + def foo(): + return {} - result = testdir.runpytest("-k", "test_background_ok") - result.assert_outcomes(passed=1) + @when('foo has a value "bar"') + def bar(foo): + foo["bar"] = "bar" + return foo["bar"] -def test_scenario_no_strict_gherkin(testdir): - """Test scenario no strict gherkin.""" - prepare_test_dir(testdir) + @when('foo is not boolean') + def not_boolean(foo): + assert foo is not bool + + + @when('foo has not a value "baz"') + def has_not_baz(foo): + assert "baz" not in foo + """ + ) testdir.makefile( ".feature", - no_strict_gherkin_scenario=""" - Feature: No strict Gherkin Scenario support + no_strict_gherkin_background=""" + Feature: No strict Gherkin Background support - Scenario: Test scenario + Background: When foo has a value "bar" And foo is not boolean And foo has not a value "baz" + Scenario: Test background + """, ) - result = testdir.runpytest("-k", "test_scenario_ok") + result = testdir.runpytest() result.assert_outcomes(passed=1) -def prepare_test_dir(testdir): +def test_scenario_no_strict_gherkin(testdir): """Test scenario no strict gherkin.""" testdir.makepyfile( test_gherkin=""" @@ -56,25 +71,13 @@ def prepare_test_dir(testdir): scenario, ) - def test_scenario_ok(request): - @scenario( - "no_strict_gherkin_scenario.feature", - "Test scenario", - ) - def test(): - pass - - test(request) - - def test_background_ok(request): - @scenario( - "no_strict_gherkin_background.feature", - "Test background", - ) - def test(): - pass + @scenario( + "no_strict_gherkin_scenario.feature", + "Test scenario", + ) + def test_scenario(): + pass - test(request) @pytest.fixture def foo(): @@ -96,3 +99,19 @@ def has_not_baz(foo): assert "baz" not in foo """ ) + + testdir.makefile( + ".feature", + no_strict_gherkin_scenario=""" + Feature: No strict Gherkin Scenario support + + Scenario: Test scenario + When foo has a value "bar" + And foo is not boolean + And foo has not a value "baz" + + """, + ) + + result = testdir.runpytest() + result.assert_outcomes(passed=1) From 078fe8f3a57c9db1eaf878c9ab7e57096a91aa39 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 17:50:31 +0200 Subject: [PATCH 12/61] Fix test --- tests/feature/test_outline.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 12d89ec73..7f87bd064 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -258,9 +258,11 @@ def test_vertical_example(testdir): Then I should have cucumbers Examples: Vertical - | start | 12 | 2 | - | eat | 5 | 1 | - | left | 7 | 1 | + | start | 12 | 2 | 2 | + | eat | 5 | 1 | 1 | + | left | 7 | 1 | 42 | + + # The last column is the control case, to verify that the scenario fails """ ), @@ -279,19 +281,13 @@ def test_vertical_example(testdir): "Outlined with vertical example table", ) def test_outline(request): - assert get_parametrize_markers_args(request.node) == ( - ["start", "eat", "left"], - [ - [12, 5.0, "7"], - [2, 1.0, "1"], - ], - ) + pass """ ) ) result = testdir.runpytest() - result.assert_outcomes(passed=2) + result.assert_outcomes(passed=2, failed=1) def test_outlined_feature(testdir): From 3bc2397e7cfeb79b189fa2c9a83c83b8fc4fbe30 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 17:55:05 +0200 Subject: [PATCH 13/61] Remove "expanded" options. It's now the default. --- CHANGES.rst | 3 + pytest_bdd/cucumber_json.py | 10 ---- pytest_bdd/gherkin_terminal_reporter.py | 11 ---- tests/feature/test_cucumber_json.py | 57 ------------------- .../feature/test_gherkin_terminal_reporter.py | 2 +- 5 files changed, 4 insertions(+), 79 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ce575309a..aeac53600 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,10 @@ Changelog Unreleased ----------- +- Examples are now always expanded in the scenarios. - Removed ``example_converters`` from ``scenario(...)`` signature +- Removed ``--cucumberjson-expanded`` and ``--cucumber-json-expanded`` options. Now the JSON report is always expanded +- Removed ``--gherkin-terminal-reporter-expanded`` option. Now the terminal report is always expanded 4.1.0 ----------- diff --git a/pytest_bdd/cucumber_json.py b/pytest_bdd/cucumber_json.py index c5ee984c3..07b5e6ea0 100644 --- a/pytest_bdd/cucumber_json.py +++ b/pytest_bdd/cucumber_json.py @@ -19,16 +19,6 @@ def add_options(parser): help="create cucumber json style report file at given path.", ) - # TODO: Remove this - group._addoption( - "--cucumberjson-expanded", - "--cucumber-json-expanded", - action="store_true", - dest="expand", - default=False, - help="expand scenario outlines into scenarios and fill in the step names", - ) - def configure(config): cucumber_json_path = config.option.cucumber_json_path diff --git a/pytest_bdd/gherkin_terminal_reporter.py b/pytest_bdd/gherkin_terminal_reporter.py index 094bc951f..f468bb525 100644 --- a/pytest_bdd/gherkin_terminal_reporter.py +++ b/pytest_bdd/gherkin_terminal_reporter.py @@ -1,9 +1,5 @@ -import re - from _pytest.terminal import TerminalReporter -from .parser import STEP_PARAM_RE - def add_options(parser): group = parser.getgroup("terminal reporting", "reporting", after="general") @@ -14,13 +10,6 @@ def add_options(parser): default=False, help=("enable gherkin output"), ) - group._addoption( - "--gherkin-terminal-reporter-expanded", - action="store_true", - dest="expand", - default=False, - help="expand scenario outlines into scenarios and fill in the step names", - ) def configure(config): diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index a8836a6a6..4284af2e0 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -226,60 +226,3 @@ def test_passing_outline(): ] assert jsonobject == expected - - -# TODO: This test is irrelevant too, since the "expanded" option is the only option now -def test_step_trace_with_expand_option(testdir): - """Test step trace.""" - testdir.makefile( - ".ini", - pytest=textwrap.dedent( - """ - [pytest] - markers = - feature-tag - scenario-outline-passing-tag - """ - ), - ) - testdir.makefile( - ".feature", - test=textwrap.dedent( - """ - @feature-tag - Feature: One scenario outline, expanded to multiple scenarios - - @scenario-outline-passing-tag - Scenario: Passing outline - Given type and value - - Examples: example1 - | type | value | - | str | hello | - | int | 42 | - | float | 1.0 | - """ - ), - ) - testdir.makepyfile( - textwrap.dedent( - """ - import pytest - from pytest_bdd import given, scenario, parsers - - @given(parsers.parse('type {type} and value {value}')) - def type_type_and_value_value(): - return 'pass' - - @scenario('test.feature', 'Passing outline') - def test_passing_outline(): - pass - """ - ) - ) - result, jsonobject = runandparse(testdir, "--cucumber-json-expanded") - result.assert_outcomes(passed=3) - - assert jsonobject[0]["elements"][0]["steps"][0]["name"] == "type str and value hello" - assert jsonobject[0]["elements"][1]["steps"][0]["name"] == "type int and value 42" - assert jsonobject[0]["elements"][2]["steps"][0]["name"] == "type float and value 1.0" diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py index 7123b9ba6..84a4a3912 100644 --- a/tests/feature/test_gherkin_terminal_reporter.py +++ b/tests/feature/test_gherkin_terminal_reporter.py @@ -225,7 +225,7 @@ def test_scenario_2(): ) ) - result = testdir.runpytest("--gherkin-terminal-reporter", "--gherkin-terminal-reporter-expanded", "-vv") + result = testdir.runpytest("--gherkin-terminal-reporter", "-vv") result.assert_outcomes(passed=1, failed=0) result.stdout.fnmatch_lines("*Scenario: Scenario example 2") result.stdout.fnmatch_lines("*Given there are {start} cucumbers".format(**example)) From c81e06d54811ae0589884417137e153c89789619 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 18:05:54 +0200 Subject: [PATCH 14/61] Minor trivial fixes --- pytest_bdd/gherkin_terminal_reporter.py | 2 +- pytest_bdd/parser.py | 3 +-- pytest_bdd/scripts.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pytest_bdd/gherkin_terminal_reporter.py b/pytest_bdd/gherkin_terminal_reporter.py index f468bb525..140328570 100644 --- a/pytest_bdd/gherkin_terminal_reporter.py +++ b/pytest_bdd/gherkin_terminal_reporter.py @@ -8,7 +8,7 @@ def add_options(parser): action="store_true", dest="gherkin_terminal_reporter", default=False, - help=("enable gherkin output"), + help="enable gherkin output", ) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 713262426..aee697ff6 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -1,4 +1,3 @@ -import io import os.path import re import textwrap @@ -479,4 +478,4 @@ def get_tags(line): return {tag.lstrip("@") for tag in line.strip().split(" @") if len(tag) > 1} -STEP_PARAM_RE = re.compile(r"\<(.+?)\>") +STEP_PARAM_RE = re.compile(r"<(.+?)>") diff --git a/pytest_bdd/scripts.py b/pytest_bdd/scripts.py index 5a136480a..155853286 100644 --- a/pytest_bdd/scripts.py +++ b/pytest_bdd/scripts.py @@ -8,7 +8,7 @@ from .generation import generate_code, parse_feature_files -MIGRATE_REGEX = re.compile(r"\s?(\w+)\s\=\sscenario\((.+)\)", flags=re.MULTILINE) +MIGRATE_REGEX = re.compile(r"\s?(\w+)\s=\sscenario\((.+)\)", flags=re.MULTILINE) def migrate_tests(args): From 719028663825125006be028dcd3ec9b39c21157a Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 18:21:59 +0200 Subject: [PATCH 15/61] Minor trivial fixes --- pytest_bdd/parser.py | 10 +++++----- pytest_bdd/scenario.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index aee697ff6..fdbe85898 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -93,7 +93,7 @@ def parse_feature(basedir, filename, encoding="utf-8"): background=None, description="", ) - scenario: typing.Optional[TemplatedScenario] = None + scenario: typing.Optional[ScenarioTemplate] = None mode = None prev_mode = None description = [] @@ -149,7 +149,7 @@ def parse_feature(basedir, filename, encoding="utf-8"): keyword, parsed_line = parse_line(clean_line) if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]: tags = get_tags(prev_line) - feature.scenarios[parsed_line] = scenario = TemplatedScenario( + feature.scenarios[parsed_line] = scenario = ScenarioTemplate( feature=feature, name=parsed_line, line_number=line_number, tags=tags ) elif mode == types.BACKGROUND: @@ -201,7 +201,7 @@ class Feature: """Feature.""" def __init__(self, scenarios, filename, rel_filename, name, tags, examples, background, line_number, description): - self.scenarios: typing.Mapping[str, TemplatedScenario] = scenarios + self.scenarios: typing.Mapping[str, ScenarioTemplate] = scenarios self.rel_filename = rel_filename self.filename = filename self.tags = tags @@ -212,8 +212,8 @@ def __init__(self, scenarios, filename, rel_filename, name, tags, examples, back self.background = background -class TemplatedScenario: - """A templated scenario. +class ScenarioTemplate: + """A scenario template. Created when parsing the feature file, it will then be combined with the examples to create a Scenario.""" diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 4e600c9e5..dc5ced0ee 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -24,7 +24,7 @@ from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path if typing.TYPE_CHECKING: - from .parser import TemplatedScenario, Scenario, Feature + from .parser import ScenarioTemplate, Scenario, Feature PYTHON_REPLACE_REGEX = re.compile(r"\W") ALPHA_REGEX = re.compile(r"^\d+_*") @@ -146,7 +146,7 @@ def _execute_scenario(feature: "Feature", scenario: "Scenario", request): def _get_scenario_decorator( - feature: "Feature", feature_name: str, templated_scenario: "TemplatedScenario", scenario_name: str + feature: "Feature", feature_name: str, templated_scenario: "ScenarioTemplate", scenario_name: str ): # HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception # when the decorator is misused. From 41b86271c95462f468331d697979e68812b65a22 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 18:33:40 +0200 Subject: [PATCH 16/61] Simplify code --- pytest_bdd/parser.py | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index fdbe85898..1206f28fe 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -241,8 +241,9 @@ def add_step(self, step): def get_params(self, builtin=False): """Get converted example params.""" + # TODO: This is wrong, we should make the cartesian product between self.feature.examples and self.examples for examples in [self.feature.examples, self.examples]: - yield examples.get_params(None, builtin=builtin) + yield examples.get_params(builtin=builtin) @property def steps(self): @@ -432,12 +433,8 @@ def add_example_row(self, param, values): self.example_params.append(param) self.vertical_examples.append(values) - # TODO: Remove `converters` - def get_params(self, converters, builtin=False): - """Get scenario pytest parametrization table. - - :param converters: `dict` of converter functions to convert parameter values - """ + def get_params(self, builtin=False): + """Get scenario pytest parametrization table.""" param_count = len(self.example_params) if self.vertical_examples and not self.examples: for value_index in range(len(self.vertical_examples[0])): @@ -447,19 +444,9 @@ def get_params(self, converters, builtin=False): self.examples.append(example) if self.examples: - params = [] - for example in self.examples: - example = list(example) - for index, param in enumerate(self.example_params): - raw_value = example[index] - if converters and param in converters: - value = converters[param](raw_value) - if not builtin or value.__class__.__module__ in {"__builtin__", "builtins"}: - example[index] = value - params.append(example) - return [self.example_params, params] - else: - return [] + return [self.example_params, self.examples] + + return [] def __bool__(self): """Bool comparison.""" From b6e442caa61cddb58cbe1d14783fd8dc7f47de45 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 22 Aug 2021 18:34:44 +0200 Subject: [PATCH 17/61] Remove unused argument --- 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 1206f28fe..bdc1768be 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -239,11 +239,11 @@ def add_step(self, step): step.scenario = self self._steps.append(step) - def get_params(self, builtin=False): + def get_params(self): """Get converted example params.""" # TODO: This is wrong, we should make the cartesian product between self.feature.examples and self.examples for examples in [self.feature.examples, self.examples]: - yield examples.get_params(builtin=builtin) + yield examples.get_params() @property def steps(self): @@ -433,7 +433,7 @@ def add_example_row(self, param, values): self.example_params.append(param) self.vertical_examples.append(values) - def get_params(self, builtin=False): + def get_params(self): """Get scenario pytest parametrization table.""" param_count = len(self.example_params) if self.vertical_examples and not self.examples: From 4ba901a80a423be58ab822f82e393d81047b4804 Mon Sep 17 00:00:00 2001 From: Oleg Pidsadnyi Date: Sun, 22 Aug 2021 19:07:31 +0200 Subject: [PATCH 18/61] PR comments --- CHANGES.rst | 3 +++ README.rst | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ce575309a..378e447de 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,9 @@ Changelog Unreleased ----------- + +This release introduces breaking changes, please refer to the :ref:`Migration from 4.x.x`. + - Removed ``example_converters`` from ``scenario(...)`` signature 4.1.0 diff --git a/README.rst b/README.rst index 724c84006..d1313c621 100644 --- a/README.rst +++ b/README.rst @@ -109,10 +109,6 @@ test_publish_article.py: Scenario decorator ------------------ -The scenario decorator can accept the following optional keyword arguments: - -* ``encoding`` - decode content of feature file in specific encoding. UTF-8 is default. - Functions decorated with the `scenario` decorator behave like a normal test function, and they will be executed after all scenario steps. You can consider it as a normal pytest test function, e.g. order fixtures there, @@ -1228,6 +1224,37 @@ and get the values from the example tables. Scenario `example_converters` are removed in favor of the converters provided on the step level. +.. _Migration from 3.x.x: + +Migration of your tests from versions 3.x.x +------------------------------------------- + + +Given steps are no longer fixtures. In case it is needed to make given step setup a fixture +the target_fixture parameter should be used. + + +.. code-block:: python + + @given("there's an article", target_fixture="article") + def there_is_an_article(): + return Article() + + +Given steps no longer have fixture parameter. In fact the step may depend on multiple fixtures. +Just normal step declaration with the dependency injection should be used. + +.. code-block:: python + + @given("there's an article") + def there_is_an_article(article): + pass + + +Strict gherkin option is removed, so the ``strict_gherkin`` parameter can be removed from the scenario decorators +as well as ``bdd_strict_gherkin`` from the ini files. + +Step validation handlers for the hook ``pytest_bdd_step_validation_error`` should be removed. License ------- From 5698c5cacec13c5fbf2893d95fc27cf35b558419 Mon Sep 17 00:00:00 2001 From: Oleg Pidsadnyi Date: Mon, 23 Aug 2021 18:59:23 +0200 Subject: [PATCH 19/61] Adding a comment that example values are no longer passed as fixtures --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d1313c621..1f36736c1 100644 --- a/README.rst +++ b/README.rst @@ -1208,7 +1208,8 @@ Migration of your tests from versions 4.x.x ------------------------------------------- The templated steps should use step argument parsers in order to match the scenario outlines -and get the values from the example tables. +and get the values from the example tables. The values from the example tables are no longer +passed as fixtures. .. code-block:: python From 86aa214531fa7ce8c2cfdefda3cabb34b74a3a56 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 29 Aug 2021 12:42:52 +0200 Subject: [PATCH 20/61] Fix bug when empty examples in Feature results in test being skipped. --- pytest_bdd/parser.py | 24 +++++++++++++-------- pytest_bdd/scenario.py | 18 +++++++--------- tests/feature/test_outline.py | 40 +++++++++++++++++------------------ 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index bdc1768be..6e338559c 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -239,12 +239,6 @@ def add_step(self, step): step.scenario = self self._steps.append(step) - def get_params(self): - """Get converted example params.""" - # TODO: This is wrong, we should make the cartesian product between self.feature.examples and self.examples - for examples in [self.feature.examples, self.examples]: - yield examples.get_params() - @property def steps(self): background = self.feature.background @@ -433,7 +427,8 @@ def add_example_row(self, param, values): self.example_params.append(param) self.vertical_examples.append(values) - def get_params(self): + # TODO: Remove this + def get_params(self) -> typing.Tuple[typing.List[str], typing.List[typing.List[str]]]: """Get scenario pytest parametrization table.""" param_count = len(self.example_params) if self.vertical_examples and not self.examples: @@ -444,9 +439,20 @@ def get_params(self): self.examples.append(example) if self.examples: - return [self.example_params, self.examples] + return self.example_params, self.examples + + return [], [] + + def as_contexts(self: "Examples"): + header, rows = self.get_params() + if not rows: + # Need to yield one result, otherwise the cartesian product later will result in an empty one + # and pytest will skip the whole test + return {} + for row in rows: + assert len(header) == len(row) - return [] + yield dict(zip(header, row)) def __bool__(self): """Bool comparison.""" diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index dc5ced0ee..333274fbf 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -24,7 +24,7 @@ from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path if typing.TYPE_CHECKING: - from .parser import ScenarioTemplate, Scenario, Feature + from .parser import ScenarioTemplate, Scenario, Feature, Examples PYTHON_REPLACE_REGEX = re.compile(r"\W") ALPHA_REGEX = re.compile(r"^\d+_*") @@ -163,18 +163,16 @@ def decorator(*args): args = get_args(fn) contexts = [] - for examples in templated_scenario.get_params(): - if not examples: - continue - header, rows = examples - for row in rows: - assert len(header) == len(row) - context = dict(zip(header, row)) + feature_contexts = templated_scenario.feature.examples.as_contexts() + scenario_contexts = templated_scenario.examples.as_contexts() + + for feature_context in feature_contexts: + for scenario_context in scenario_contexts: + context = {**feature_context, **scenario_context} contexts.append(pytest.param(context, id="-".join(context.values()))) - if not contexts: - contexts = [pytest.param({}, id="")] + assert contexts, "Programming error: contexts must be non empty, otherwise the test will be skipped" # We need to tell pytest that the original function requires its fixtures, # otherwise indirect fixtures would not work. diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 7f87bd064..318ad6082 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -318,45 +318,43 @@ def test_outlined_feature(testdir): testdir.makepyfile( textwrap.dedent( """\ - from pytest_bdd.utils import get_parametrize_markers_args - from pytest_bdd import given, when, then, scenario + from pytest_bdd import given, when, then, scenario, parsers @scenario( "outline.feature", "Outlined given, when, thens", ) def test_outline(request): - assert get_parametrize_markers_args(request.node) == ( - ["start", "eat", "left"], - [[12, 5.0, "7"], [5, 4.0, "1"]], - ["fruits"], - [["oranges"], ["apples"]], - ) - - @given("there are ", target_fixture="start_fruits") + # This won't work anymore + # assert get_parametrize_markers_args(request.node) == ( + # ["start", "eat", "left"], + # [[12, 5.0, "7"], [5, 4.0, "1"]], + # ["fruits"], + # [["oranges"], ["apples"]], + # ) + pass + + @given(parsers.parse("there are {start} {fruits}"), target_fixture="start_fruits") def start_fruits(start, fruits): - assert isinstance(start, int) - return {fruits: dict(start=start)} + return {fruits: {"start": int(start)}} - @when("I eat ") + @when(parsers.parse("I eat {eat} {fruits}")) def eat_fruits(start_fruits, eat, fruits): - assert isinstance(eat, float) - start_fruits[fruits]["eat"] = eat + start_fruits[fruits]["eat"] = float(eat) - @then("I should have ") + @then(parsers.parse("I should have {left} {fruits}")) def should_have_left_fruits(start_fruits, start, eat, left, fruits): - assert isinstance(left, str) - assert start - eat == int(left) - assert start_fruits[fruits]["start"] == start - assert start_fruits[fruits]["eat"] == eat + # TODO: it seems that the "start" fixture is injected, but it should not exists! + assert int(left) == start_fruits[fruits]["start"] - start_fruits[fruits]["eat"] """ ) ) - result = testdir.runpytest() + result = testdir.runpytest("-s") result.assert_outcomes(passed=4) + # TODO: Find a way to assert that the tests were run with the expected parametrizations def test_outline_with_escaped_pipes(testdir): From 5ce621d6787fbb8326adb4b7119e06848747d397 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 29 Aug 2021 12:47:03 +0200 Subject: [PATCH 21/61] fix no value being yielded --- pytest_bdd/parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 6e338559c..42fa891b9 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -448,7 +448,8 @@ def as_contexts(self: "Examples"): if not rows: # Need to yield one result, otherwise the cartesian product later will result in an empty one # and pytest will skip the whole test - return {} + yield {} + return for row in rows: assert len(header) == len(row) From 67d1868b0c8332e2e957cedb8a1c8dbba04a53a2 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 29 Aug 2021 12:52:27 +0200 Subject: [PATCH 22/61] Fix iterator being consumed on the first time --- pytest_bdd/scenario.py | 6 ++++-- tests/feature/test_outline.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 333274fbf..cf1dbd329 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -164,8 +164,10 @@ def decorator(*args): contexts = [] - feature_contexts = templated_scenario.feature.examples.as_contexts() - scenario_contexts = templated_scenario.examples.as_contexts() + # We need to evaluate these iterators and store them as lists, otherwise + # we won't be able to do the cartesian product later (the second iterator will be consumed) + feature_contexts = list(templated_scenario.feature.examples.as_contexts()) + scenario_contexts = list(templated_scenario.examples.as_contexts()) for feature_context in feature_contexts: for scenario_context in scenario_contexts: diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 318ad6082..e24f76fce 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -324,7 +324,8 @@ def test_outlined_feature(testdir): "outline.feature", "Outlined given, when, thens", ) - def test_outline(request): + def test_outline(request, _pytest_bdd_example): + print("parametrization: ", _pytest_bdd_example) # This won't work anymore # assert get_parametrize_markers_args(request.node) == ( # ["start", "eat", "left"], From 6acd8043ad2d3560b9ba1b6509c64d5f6b6dc08e Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 29 Aug 2021 12:54:11 +0200 Subject: [PATCH 23/61] Fix test --- tests/feature/test_outline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index e24f76fce..a41733745 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -388,7 +388,7 @@ def test_outline_with_escaped_pipes(testdir): """\ import base64 - from pytest_bdd import scenario, given, when, then + from pytest_bdd import scenario, given, when, then, parsers from pytest_bdd.utils import get_parametrize_markers_args @@ -397,12 +397,12 @@ def test_outline_with_escaped_pipe_character(request): pass - @given("We have strings and ") + @given(parsers.parse("We have strings {string1} and {string2}")) def we_have_strings_string1_and_string2(string1, string2): pass - @then(" should be the base64 encoding of ") + @then(parsers.parse("{string2} should be the base64 encoding of {string1}")) def string2_should_be_base64_encoding_of_string1(string2, string1): assert string1.encode() == base64.b64decode(string2.encode()) From 1ee3b4b6fbc6db3c04f52cb5ba7b507ad2acec6c Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 29 Aug 2021 13:05:55 +0200 Subject: [PATCH 24/61] Fix test, but I'm not sure what we want to do with this behaviour. --- tests/feature/test_parametrized.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index d650f4f6b..26b655df1 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -1,6 +1,8 @@ import textwrap +# TODO: This test was testing a behaviour that is different now. Do we want to support it? +# I think not, but not sure def test_parametrized(testdir): """Test parametrized scenario.""" testdir.makefile( @@ -9,9 +11,9 @@ def test_parametrized(testdir): """\ Feature: Parametrized scenario Scenario: Parametrized given, when, thens - Given there are cucumbers - When I eat cucumbers - Then I should have cucumbers + Given there are {start} cucumbers + When I eat {eat} cucumbers + Then I should have {left} cucumbers """ ), ) @@ -20,7 +22,7 @@ def test_parametrized(testdir): textwrap.dedent( """\ import pytest - from pytest_bdd import given, when, then, scenario + from pytest_bdd import given, when, then, scenario, parsers @pytest.mark.parametrize(["start", "eat", "left"], [(12, 5, 7)]) @@ -38,24 +40,24 @@ def foo_bar(request): def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar): pass - @given("there are cucumbers", target_fixture="start_cucumbers") + @given(parsers.parse("there are {start} cucumbers"), target_fixture="start_cucumbers") def start_cucumbers(start): return dict(start=start) - @when("I eat cucumbers") + @when(parsers.parse("I eat {eat} cucumbers")) def eat_cucumbers(start_cucumbers, start, eat): start_cucumbers["eat"] = eat - @then("I should have cucumbers") + @then(parsers.parse("I should have {left} 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 + assert int(left) == start_cucumbers["start"] - start_cucumbers["eat"] """ ) ) - result = testdir.runpytest() + result = testdir.runpytest("-s") result.assert_outcomes(passed=3) + # TODO: We should test the parametrization of each test item, otherwise it's quite useless From 5a9783315cef1e8127580ff54f59fe9e042f93a9 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Thu, 16 Sep 2021 18:50:07 +0200 Subject: [PATCH 25/61] Add utility functions to be able to inspect tests run by the pytester. --- pytest_bdd/utils.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/utils.py b/pytest_bdd/utils.py index fa3866a6d..20d3bcf78 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -1,8 +1,15 @@ """Various utility functions.""" - +import base64 +import pickle +import re +import warnings from inspect import getframeinfo from inspect import signature as _signature from sys import _getframe +import typing + +if typing.TYPE_CHECKING: + from _pytest.pytester import RunResult CONFIG_STACK = [] @@ -20,6 +27,10 @@ def get_args(func): def get_parametrize_markers_args(node): + warnings.warn( + "get_parametrize_markers_args is deprecated. Use dump_obj and collect_dumped_objects instead.", + DeprecationWarning, + ) return tuple(arg for mark in node.iter_markers("parametrize") for arg in mark.args) @@ -40,3 +51,25 @@ def get_caller_module_path(depth=2): """ frame = _getframe(depth) return getframeinfo(frame, context=0).filename + + +_DUMP_START = "_pytest_bdd_>>>" +_DUMP_END = "<<<_pytest_bdd_" + + +def dump_obj(*objects): + """Dump objects to stdout so that they can be inspected by the test suite.""" + for obj in objects: + dump = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL) + encoded = base64.b64encode(dump).decode("ascii") + print(f"{_DUMP_START}{encoded}{_DUMP_END}") + + +def collect_dumped_objects(result: "RunResult"): + """Parse all the objects dumped with `dump_object` from the result. + + Note: You must run the result with output to stdout enabled. + For example, using ``testdir.runpytest("-s")``. + """ + payloads = re.findall(rf"{_DUMP_START}(.*?){_DUMP_END}", str(result.stdout)) + return [pickle.loads(base64.b64decode(payload)) for payload in payloads] From 660bac087d55af92e315e8b19bc1d2b8c5fa02a9 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Thu, 16 Sep 2021 18:50:57 +0200 Subject: [PATCH 26/61] Improve test inspection --- tests/feature/test_outline_empty_values.py | 50 ++++++++++++++-------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/tests/feature/test_outline_empty_values.py b/tests/feature/test_outline_empty_values.py index 0285b8086..5cdbbff31 100644 --- a/tests/feature/test_outline_empty_values.py +++ b/tests/feature/test_outline_empty_values.py @@ -1,24 +1,27 @@ """Scenario Outline with empty example values tests.""" import textwrap +from pytest_bdd.utils import collect_dumped_objects STEPS = """\ -from pytest_bdd import given, when, then +from pytest_bdd import given, when, then, parsers +from pytest_bdd.utils import dump_obj +# Using `parsers.re` so that we can match empty values -@given("there are cucumbers") +@given(parsers.re("there are (?P.*?) cucumbers")) def start_cucumbers(start): - pass + dump_obj(start) -@when("I eat cucumbers") +@when(parsers.re("I eat (?P.*?) cucumbers")) def eat_cucumbers(eat): - pass + dump_obj(eat) -@then("I should have cucumbers") +@then(parsers.re("I should have (?P.*?) cucumbers")) def should_have_left_cucumbers(left): - pass + dump_obj(left) """ @@ -45,18 +48,28 @@ def test_scenario_with_empty_example_values(testdir): testdir.makepyfile( textwrap.dedent( """\ - from pytest_bdd.utils import get_parametrize_markers_args + from pytest_bdd.utils import dump_obj from pytest_bdd import scenario + import json @scenario("outline.feature", "Outlined with empty example values") - def test_outline(request): - assert get_parametrize_markers_args(request.node) == ([u"start", u"eat", u"left"], [["#", "", ""]]) - + # TODO: It seems that we can't have: + # def test_outline(_pytest_bdd_example, start_cucumbers, eat_cucumbers, left_cucumbers): + # This can be quite a problem for old usages. Think about what to do. + # Maybe just mention this in the release notes too. And mention that the `target_fixtures` can be accessed + # by the scenario test function using ``request.getfixturevalue(...)`` + # The scenario test function should be not used anyway. + def test_outline(): + pass """ ) ) - result = testdir.runpytest() + result = testdir.runpytest("-s") result.assert_outcomes(passed=1) + [start, eat, left] = collect_dumped_objects(result) + assert start == "#" + assert eat == "" + assert left == "" def test_scenario_with_empty_example_values_vertical(testdir): @@ -82,15 +95,18 @@ def test_scenario_with_empty_example_values_vertical(testdir): testdir.makepyfile( textwrap.dedent( """\ - from pytest_bdd.utils import get_parametrize_markers_args + from pytest_bdd.utils import dump_obj from pytest_bdd import scenario @scenario("outline.feature", "Outlined with empty example values vertical") - def test_outline(request): - assert get_parametrize_markers_args(request.node) == ([u"start", u"eat", u"left"], [["#", "", ""]]) - + def test_outline(): + pass """ ) ) - result = testdir.runpytest() + result = testdir.runpytest("-s") + [start, eat, left] = collect_dumped_objects(result) + assert start == "#" + assert eat == "" + assert left == "" result.assert_outcomes(passed=1) From d930d51a966052a135a60bf1896f2105766adfee Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Thu, 16 Sep 2021 18:54:12 +0200 Subject: [PATCH 27/61] Remove unused import, use better timer --- pytest_bdd/reporting.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pytest_bdd/reporting.py b/pytest_bdd/reporting.py index d4e9e81a5..a4b7d0c59 100644 --- a/pytest_bdd/reporting.py +++ b/pytest_bdd/reporting.py @@ -6,8 +6,6 @@ import time -from .utils import get_parametrize_markers_args - class StepReport: """Step excecution report.""" @@ -21,7 +19,7 @@ def __init__(self, step): :param pytest_bdd.parser.Step step: Step. """ self.step = step - self.started = time.time() + self.started = time.perf_counter() def serialize(self): """Serialize the step excecution report. @@ -43,7 +41,7 @@ def finalize(self, failed): :param bool failed: Wheither the step excecution is failed. """ - self.stopped = time.time() + self.stopped = time.perf_counter() self.failed = failed @property From 98a224b07803613602d1369b3086bf67aeb22429 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Thu, 16 Sep 2021 18:57:05 +0200 Subject: [PATCH 28/61] Fix typos --- pytest_bdd/reporting.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pytest_bdd/reporting.py b/pytest_bdd/reporting.py index a4b7d0c59..b6f7d9c8b 100644 --- a/pytest_bdd/reporting.py +++ b/pytest_bdd/reporting.py @@ -1,6 +1,6 @@ """Reporting functionality. -Collection of the scenario excecution statuses, timing and other information +Collection of the scenario execution statuses, timing and other information that enriches the pytest test reporting. """ @@ -8,7 +8,7 @@ class StepReport: - """Step excecution report.""" + """Step execution report.""" failed = False stopped = None @@ -22,9 +22,9 @@ def __init__(self, step): self.started = time.perf_counter() def serialize(self): - """Serialize the step excecution report. + """Serialize the step execution report. - :return: Serialized step excecution report. + :return: Serialized step execution report. :rtype: dict """ return { @@ -39,16 +39,16 @@ def serialize(self): def finalize(self, failed): """Stop collecting information and finalize the report. - :param bool failed: Wheither the step excecution is failed. + :param bool failed: Whether the step execution is failed. """ self.stopped = time.perf_counter() self.failed = failed @property def duration(self): - """Step excecution duration. + """Step execution duration. - :return: Step excecution duration. + :return: Step execution duration. :rtype: float """ if self.stopped is None: @@ -87,7 +87,7 @@ def add_step_report(self, step_report): self.step_reports.append(step_report) def serialize(self): - """Serialize scenario excecution report in order to transfer reportin from nodes in the distributed mode. + """Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode. :return: Serialized report. :rtype: dict From 90ad0fa9656e89c7031bce87874a681a21d997b2 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 16:56:04 +0200 Subject: [PATCH 29/61] Fix and simplify tests --- pytest_bdd/scenario.py | 1 + tests/feature/test_outline.py | 103 ++++++++++++++++++++-------------- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index cf1dbd329..3c6a34636 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -42,6 +42,7 @@ def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None) if not match: continue + # Todo: is `converters` still a thing anywhere? Remove it converters = getattr(fixturedef.func, "converters", {}) for arg, value in parser.parse_arguments(name).items(): if arg in converters: diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index a41733745..ae1844170 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -1,16 +1,19 @@ """Scenario Outline tests.""" import textwrap +from pytest_bdd.utils import collect_dumped_objects from tests.utils import assert_outcomes STEPS = """\ from pytest_bdd import parsers, given, when, then +from pytest_bdd.utils import dump_obj @given(parsers.parse("there are {start} cucumbers"), target_fixture="start_cucumbers") def start_cucumbers(start): assert isinstance(start, str) start = int(start) + dump_obj(start) return dict(start=start) @@ -18,6 +21,7 @@ def start_cucumbers(start): def eat_cucumbers(start_cucumbers, eat): assert isinstance(eat, str) eat = int(eat) + dump_obj(eat) start_cucumbers["eat"] = eat @@ -25,6 +29,7 @@ def eat_cucumbers(start_cucumbers, eat): def should_have_left_cucumbers(start_cucumbers, left): assert isinstance(left, str) left = int(left) + dump_obj(left) assert left == start_cucumbers["start"] - start_cucumbers["eat"] """ @@ -56,7 +61,6 @@ def test_outlined(testdir): testdir.makepyfile( textwrap.dedent( """\ - from pytest_bdd.utils import get_parametrize_markers_args from pytest_bdd import scenario @scenario( @@ -222,7 +226,6 @@ def test_outlined_with_other_fixtures(testdir): textwrap.dedent( """\ import pytest - from pytest_bdd.utils import get_parametrize_markers_args from pytest_bdd import scenario @@ -273,21 +276,27 @@ def test_vertical_example(testdir): testdir.makepyfile( textwrap.dedent( """\ - from pytest_bdd.utils import get_parametrize_markers_args from pytest_bdd import scenario @scenario( "outline.feature", "Outlined with vertical example table", ) - def test_outline(request): + def test_outline(): pass - """ ) ) - result = testdir.runpytest() + result = testdir.runpytest("-s") result.assert_outcomes(passed=2, failed=1) + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 12, 5, 7, + 2, 1, 1, + 2, 1, 42, + ] + # fmt: on def test_outlined_feature(testdir): @@ -319,43 +328,50 @@ def test_outlined_feature(testdir): textwrap.dedent( """\ from pytest_bdd import given, when, then, scenario, parsers + from pytest_bdd.utils import dump_obj @scenario( "outline.feature", "Outlined given, when, thens", ) - def test_outline(request, _pytest_bdd_example): - print("parametrization: ", _pytest_bdd_example) - # This won't work anymore - # assert get_parametrize_markers_args(request.node) == ( - # ["start", "eat", "left"], - # [[12, 5.0, "7"], [5, 4.0, "1"]], - # ["fruits"], - # [["oranges"], ["apples"]], - # ) + def test_outline(): pass @given(parsers.parse("there are {start} {fruits}"), target_fixture="start_fruits") def start_fruits(start, fruits): - return {fruits: {"start": int(start)}} + start = int(start) + dump_obj(start, fruits) + return {fruits: {"start": start}} @when(parsers.parse("I eat {eat} {fruits}")) def eat_fruits(start_fruits, eat, fruits): - start_fruits[fruits]["eat"] = float(eat) + eat = float(eat) + dump_obj(eat, fruits) + start_fruits[fruits]["eat"] = eat @then(parsers.parse("I should have {left} {fruits}")) def should_have_left_fruits(start_fruits, start, eat, left, fruits): # TODO: it seems that the "start" fixture is injected, but it should not exists! - assert int(left) == start_fruits[fruits]["start"] - start_fruits[fruits]["eat"] + left = int(left) + dump_obj(left, fruits) + assert left == start_fruits[fruits]["start"] - start_fruits[fruits]["eat"] """ ) ) result = testdir.runpytest("-s") result.assert_outcomes(passed=4) - # TODO: Find a way to assert that the tests were run with the expected parametrizations + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 12, "oranges", 5, "oranges", 7, "oranges", + 12, "apples", 5, "apples", 7, "apples", + 5, "oranges", 4, "oranges", 1, "oranges", + 5, "apples", 4, "apples", 1, "apples", + ] + # fmt: on def test_outline_with_escaped_pipes(testdir): @@ -367,18 +383,18 @@ def test_outline_with_escaped_pipes(testdir): Feature: Outline With Special characters Scenario Outline: Outline with escaped pipe character - Given We have strings and - Then should be the base64 encoding of + # Just print the string so that we can assert later what it was by reading the output + Given I print the Examples: - | string1 | string2 | - | bork | Ym9yaw== | - | \|bork | fGJvcms= | - | bork \| | Ym9yayB8 | - | bork\|\|bork | Ym9ya3x8Ym9yaw== | - | \| | fA== | - | bork \\ | Ym9yayAgICAgIFxc | - | bork \\\| | Ym9yayAgICBcXHw= | + | string | + | bork | + | \|bork | + | bork \| | + | bork\|\|bork | + | \| | + | bork \\ | + | bork \\\| | """ ), ) @@ -386,10 +402,8 @@ def test_outline_with_escaped_pipes(testdir): testdir.makepyfile( textwrap.dedent( """\ - import base64 - - from pytest_bdd import scenario, given, when, then, parsers - from pytest_bdd.utils import get_parametrize_markers_args + from pytest_bdd import scenario, given, parsers + from pytest_bdd.utils import dump_obj @scenario("outline.feature", "Outline with escaped pipe character") @@ -397,17 +411,20 @@ def test_outline_with_escaped_pipe_character(request): pass - @given(parsers.parse("We have strings {string1} and {string2}")) - def we_have_strings_string1_and_string2(string1, string2): - pass - - - @then(parsers.parse("{string2} should be the base64 encoding of {string1}")) - def string2_should_be_base64_encoding_of_string1(string2, string1): - assert string1.encode() == base64.b64decode(string2.encode()) - + @given(parsers.parse("I print the {string}")) + def i_print_the_string(string): + dump_obj(string) """ ) ) - result = testdir.runpytest() + result = testdir.runpytest("-s") result.assert_outcomes(passed=7) + assert collect_dumped_objects(result) == [ + r"bork", + r"|bork", + r"bork |", + r"bork||bork", + r"|", + r"bork \\", + r"bork \\|", + ] From 60285ad1046ff0f59a9755ef37bab92efab0fbed Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 16:57:31 +0200 Subject: [PATCH 30/61] Remove unused code --- pytest_bdd/utils.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pytest_bdd/utils.py b/pytest_bdd/utils.py index 20d3bcf78..4874def74 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -2,9 +2,8 @@ import base64 import pickle import re -import warnings from inspect import getframeinfo -from inspect import signature as _signature +from inspect import signature from sys import _getframe import typing @@ -22,18 +21,10 @@ def get_args(func): :return: A list of argument names. :rtype: list """ - params = _signature(func).parameters.values() + params = signature(func).parameters.values() return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD] -def get_parametrize_markers_args(node): - warnings.warn( - "get_parametrize_markers_args is deprecated. Use dump_obj and collect_dumped_objects instead.", - DeprecationWarning, - ) - return tuple(arg for mark in node.iter_markers("parametrize") for arg in mark.args) - - def get_caller_module_locals(depth=2): """Get the caller module locals dictionary. From 9010a16f7331bbeea86b18cb7a529fbe831a2380 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 17:33:17 +0200 Subject: [PATCH 31/61] Fix test items ending with "[]" (for example, "test_scenarios.py::test_already_bound[]"). --- pytest_bdd/parser.py | 5 --- pytest_bdd/plugin.py | 14 +++++++++ pytest_bdd/scenario.py | 47 +++++++++++++++++++---------- tests/feature/test_cucumber_json.py | 4 +-- tests/feature/test_report.py | 4 +-- 5 files changed, 49 insertions(+), 25 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 42fa891b9..5f351e70d 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -445,11 +445,6 @@ def get_params(self) -> typing.Tuple[typing.List[str], typing.List[typing.List[s def as_contexts(self: "Examples"): header, rows = self.get_params() - if not rows: - # Need to yield one result, otherwise the cartesian product later will result in an empty one - # and pytest will skip the whole test - yield {} - return for row in rows: assert len(header) == len(row) diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index 20ac37eb6..e82a2b1c3 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -25,6 +25,20 @@ def trace(): pytest.set_trace() +@pytest.fixture +def _pytest_bdd_example(): + """The current scenario outline parametrization. + + This is used internally by pytest_bdd. + + If no outline is used, we just return an empty dict to render + the current template without any actual variable. + Otherwise pytest_bdd will add all the context variables in this fixture + from the example definitions in the feature file. + """ + return {} + + def pytest_addoption(parser): """Add pytest-bdd options.""" add_bdd_ini(parser) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 3c6a34636..211379109 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -24,7 +24,8 @@ from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path if typing.TYPE_CHECKING: - from .parser import ScenarioTemplate, Scenario, Feature, Examples + from .parser import ScenarioTemplate, Scenario, Feature + from _pytest.mark.structures import ParameterSet PYTHON_REPLACE_REGEX = re.compile(r"\W") ALPHA_REGEX = re.compile(r"^\d+_*") @@ -163,30 +164,23 @@ def decorator(*args): [fn] = args args = get_args(fn) - contexts = [] - - # We need to evaluate these iterators and store them as lists, otherwise - # we won't be able to do the cartesian product later (the second iterator will be consumed) - feature_contexts = list(templated_scenario.feature.examples.as_contexts()) - scenario_contexts = list(templated_scenario.examples.as_contexts()) - - for feature_context in feature_contexts: - for scenario_context in scenario_contexts: - context = {**feature_context, **scenario_context} - contexts.append(pytest.param(context, id="-".join(context.values()))) - - assert contexts, "Programming error: contexts must be non empty, otherwise the test will be skipped" - # We need to tell pytest that the original function requires its fixtures, # otherwise indirect fixtures would not work. @pytest.mark.usefixtures(*args) - @pytest.mark.parametrize("_pytest_bdd_example", contexts) # Parametrize the scenario outlines def scenario_wrapper(request, _pytest_bdd_example): scenario = templated_scenario.render(_pytest_bdd_example) _execute_scenario(feature, scenario, request) fixture_values = [request.getfixturevalue(arg) for arg in args] return fn(*fixture_values) + example_parametrizations = collect_example_parametrizations(templated_scenario) + if example_parametrizations is not None: + # Parametrize the scenario outlines + scenario_wrapper = pytest.mark.parametrize( + "_pytest_bdd_example", + example_parametrizations, + )(scenario_wrapper) + for tag in templated_scenario.tags.union(feature.tags): config = CONFIG_STACK[-1] config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper) @@ -199,6 +193,27 @@ def scenario_wrapper(request, _pytest_bdd_example): return decorator +def collect_example_parametrizations( + templated_scenario: "ScenarioTemplate", +) -> "typing.Optional[typing.List[ParameterSet]]": + # We need to evaluate these iterators and store them as lists, otherwise + # we won't be able to do the cartesian product later (the second iterator will be consumed) + feature_contexts = list(templated_scenario.feature.examples.as_contexts()) + scenario_contexts = list(templated_scenario.examples.as_contexts()) + + contexts = [ + {**feature_context, **scenario_context} + # We must make sure that we always have at least one element in each list, otherwise + # the cartesian product will result in an empty list too, even if one of the 2 sets + # is non empty. + for feature_context in feature_contexts or [{}] + for scenario_context in scenario_contexts or [{}] + ] + if contexts == [{}]: + return + 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): """Scenario decorator. diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index 4284af2e0..e34208183 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -113,7 +113,7 @@ def test_passing_outline(): "elements": [ { "description": "", - "id": "test_passing[]", + "id": "test_passing", "keyword": "Scenario", "line": 5, "name": "Passing", @@ -138,7 +138,7 @@ def test_passing_outline(): }, { "description": "", - "id": "test_failing[]", + "id": "test_failing", "keyword": "Scenario", "line": 10, "name": "Failing", diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 1d250391e..d29ba9f2a 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -97,7 +97,7 @@ def should_have_left_cucumbers(start_cucumbers, left): ) result = testdir.inline_run("-vvl") assert result.ret - report = result.matchreport("test_passing[]", when="call").scenario + report = result.matchreport("test_passing", when="call").scenario expected = { "feature": { "description": "", @@ -132,7 +132,7 @@ def should_have_left_cucumbers(start_cucumbers, left): assert report == expected - report = result.matchreport("test_failing[]", when="call").scenario + report = result.matchreport("test_failing", when="call").scenario expected = { "feature": { "description": "", From 49465894583add6c0bc15dca3715170cd133a33f Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 17:52:17 +0200 Subject: [PATCH 32/61] Fix tests --- tests/feature/test_report.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index d29ba9f2a..8fd9236bf 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -184,7 +184,7 @@ def should_have_left_cucumbers(start_cucumbers, left): "failed": False, "keyword": "Given", "line_number": 15, - "name": "there are cucumbers", + "name": "there are 12 cucumbers", "type": "given", }, { @@ -192,7 +192,7 @@ def should_have_left_cucumbers(start_cucumbers, left): "failed": False, "keyword": "When", "line_number": 16, - "name": "I eat cucumbers", + "name": "I eat 5 cucumbers", "type": "when", }, { @@ -200,7 +200,7 @@ def should_have_left_cucumbers(start_cucumbers, left): "failed": False, "keyword": "Then", "line_number": 17, - "name": "I should have cucumbers", + "name": "I should have 7 cucumbers", "type": "then", }, ], @@ -226,7 +226,7 @@ def should_have_left_cucumbers(start_cucumbers, left): "failed": False, "keyword": "Given", "line_number": 15, - "name": "there are cucumbers", + "name": "there are 5 cucumbers", "type": "given", }, { @@ -234,7 +234,7 @@ def should_have_left_cucumbers(start_cucumbers, left): "failed": False, "keyword": "When", "line_number": 16, - "name": "I eat cucumbers", + "name": "I eat 4 cucumbers", "type": "when", }, { @@ -242,7 +242,7 @@ def should_have_left_cucumbers(start_cucumbers, left): "failed": False, "keyword": "Then", "line_number": 17, - "name": "I should have cucumbers", + "name": "I should have 1 cucumbers", "type": "then", }, ], From c7c04f95f36ad99ce993f970b398f7a9a88211b1 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 18:03:12 +0200 Subject: [PATCH 33/61] Fix test --- tests/feature/test_report.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 8fd9236bf..13b6705ac 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -251,13 +251,13 @@ def should_have_left_cucumbers(start_cucumbers, left): assert report == expected -def test_complex_types(testdir): +def test_complex_types(testdir, pytestconfig): """Test serialization of the complex types.""" - try: - import execnet.gateway_base - except ImportError: + if not pytestconfig.pluginmanager.has_plugin("xdist"): pytest.skip("Execnet not installed") + import execnet.gateway_base + testdir.makefile( ".feature", test=textwrap.dedent( @@ -277,9 +277,9 @@ def test_complex_types(testdir): textwrap.dedent( """ import pytest - from pytest_bdd import given, when, then, scenario + from pytest_bdd import given, when, then, scenario, parsers - class Point(object): + class Point: def __init__(self, x, y): self.x = x @@ -292,10 +292,9 @@ def parse(cls, value): class Alien(object): pass - @given('there is a coordinate ') - def point(point): - assert isinstance(point, Point) - return point + @given(parsers.parse('there is a coordinate {point}'), target_fixture="point") + def given_there_is_a_point(point): + return Point.parse(point) @pytest.mark.parametrize('alien', [Alien()]) @@ -307,6 +306,7 @@ def test_complex(alien): ) ) result = testdir.inline_run("-vvl") - report = result.matchreport("test_complex[point0-alien0]", when="call") + report = result.matchreport("test_complex[10,20-alien0]", when="call") + assert report.passed assert execnet.gateway_base.dumps(report.item) assert execnet.gateway_base.dumps(report.scenario) From 9eb85117819e484b779f9e8210e76c536057a438 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 18:06:33 +0200 Subject: [PATCH 34/61] Update to latest python 3.10 version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e345de50a..7692fd91c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", 3.10.0-beta.4] + python-version: ["3.6", "3.7", "3.8", "3.9", 3.10.0-rc.2] steps: - uses: actions/checkout@v2 From d6dacc9a3c9f0be6a0b36f9aefe947e12e0c7624 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 23:34:34 +0200 Subject: [PATCH 35/61] Fix pytest < 6.2 compatibility --- pytest_bdd/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/utils.py b/pytest_bdd/utils.py index 4874def74..6722d270c 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -62,5 +62,6 @@ def collect_dumped_objects(result: "RunResult"): Note: You must run the result with output to stdout enabled. For example, using ``testdir.runpytest("-s")``. """ - payloads = re.findall(rf"{_DUMP_START}(.*?){_DUMP_END}", str(result.stdout)) + stdout = result.stdout.str() # pytest < 6.2, otherwise we could just do str(result.stdout) + payloads = re.findall(rf"{_DUMP_START}(.*?){_DUMP_END}", stdout) return [pickle.loads(base64.b64decode(payload)) for payload in payloads] From 6783783fdf07d72baf62bbe8055b8920a3596a15 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 23:47:35 +0200 Subject: [PATCH 36/61] Fix readme --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 1f36736c1..1e60080d4 100644 --- a/README.rst +++ b/README.rst @@ -555,13 +555,13 @@ The code will look like: pass - @given(parsers.parse("there are {start} cucumbers", target_fixture="start_cucumbers")) + @given(parsers.parse("there are {start:d} cucumbers", target_fixture="start_cucumbers")) def start_cucumbers(start): assert isinstance(start, int) return dict(start=start) - @when(parsers.parse("I eat {eat} cucumbers")) + @when(parsers.parse("I eat {eat:g} cucumbers")) def eat_cucumbers(start_cucumbers, eat): assert isinstance(eat, float) start_cucumbers["eat"] = eat @@ -666,17 +666,17 @@ The code will look like: """We don't need to do anything here, everything will be managed by the scenario decorator.""" - @given(parsers.parse("there are {start} cucumbers"), target_fixture="start_cucumbers") + @given(parsers.parse("there are {start:d} cucumbers"), target_fixture="start_cucumbers") def start_cucumbers(start): return dict(start=start) - @when(parsers.parse("I eat {eat} cucumbers")) + @when(parsers.parse("I eat {eat:d} cucumbers")) def eat_cucumbers(start_cucumbers, start, eat): start_cucumbers["eat"] = eat - @then(parsers.parse("I should have {left} cucumbers")) + @then(parsers.parse("I should have {left:d} cucumbers")) def should_have_left_cucumbers(start_cucumbers, start, eat, left): assert start - eat == left assert start_cucumbers["start"] == start From 68cd139903475cbe3f49e846a5b83fa2f147a770 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 23:48:49 +0200 Subject: [PATCH 37/61] use f-string --- pytest_bdd/gherkin_terminal_reporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_bdd/gherkin_terminal_reporter.py b/pytest_bdd/gherkin_terminal_reporter.py index 140328570..29f2d4deb 100644 --- a/pytest_bdd/gherkin_terminal_reporter.py +++ b/pytest_bdd/gherkin_terminal_reporter.py @@ -82,7 +82,7 @@ def pytest_runtest_logreport(self, report): self._tw.write(report.scenario["name"], **scenario_markup) self._tw.write("\n") for step in report.scenario["steps"]: - self._tw.write(" {} {}\n".format(step["keyword"], step["name"]), **scenario_markup) + self._tw.write(f" {step['keyword']} {step['name']}\n", **scenario_markup) self._tw.write(" " + word, **word_markup) self._tw.write("\n\n") else: From 7ed5bf687da14bc45ec3e67b27801678832708c3 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sat, 18 Sep 2021 23:59:16 +0200 Subject: [PATCH 38/61] Inline method get_params() --- pytest_bdd/parser.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 5f351e70d..0c9fcacd9 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -427,9 +427,7 @@ def add_example_row(self, param, values): self.example_params.append(param) self.vertical_examples.append(values) - # TODO: Remove this - def get_params(self) -> typing.Tuple[typing.List[str], typing.List[typing.List[str]]]: - """Get scenario pytest parametrization table.""" + def as_contexts(self) -> typing.Iterable[typing.Dict[str, typing.Any]]: param_count = len(self.example_params) if self.vertical_examples and not self.examples: for value_index in range(len(self.vertical_examples[0])): @@ -438,13 +436,11 @@ def get_params(self) -> typing.Tuple[typing.List[str], typing.List[typing.List[s example.append(self.vertical_examples[param_index][value_index]) self.examples.append(example) - if self.examples: - return self.example_params, self.examples + if not self.examples: + return - return [], [] + header, rows = self.example_params, self.examples - def as_contexts(self: "Examples"): - header, rows = self.get_params() for row in rows: assert len(header) == len(row) From e7b8ecbaf919c9ff0bc273f421dc8941a7eda10c Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 00:00:30 +0200 Subject: [PATCH 39/61] Move re to the beginning of the module --- pytest_bdd/parser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 0c9fcacd9..ad6b89db4 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -7,6 +7,7 @@ from . import types, exceptions SPLIT_LINE_RE = re.compile(r"(?") COMMENT_RE = re.compile(r"(^|(?<=\s))#") STEP_PREFIXES = [ ("Feature: ", types.FEATURE), @@ -461,6 +462,3 @@ def get_tags(line): if not line or not line.strip().startswith("@"): return set() return {tag.lstrip("@") for tag in line.strip().split(" @") if len(tag) > 1} - - -STEP_PARAM_RE = re.compile(r"<(.+?)>") From e638007a1eed35201fdad661f5b2d0abdc109bc7 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 00:08:08 +0200 Subject: [PATCH 40/61] Update TODO --- pytest_bdd/scenario.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 211379109..fc0c6e6ba 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -43,7 +43,8 @@ def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None) if not match: continue - # Todo: is `converters` still a thing anywhere? Remove it + # TODO: maybe `converters` should be part of the SterParser.__init__(), + # and used by StepParser.parse_arguments() method converters = getattr(fixturedef.func, "converters", {}) for arg, value in parser.parse_arguments(name).items(): if arg in converters: From 0e4d2718e0fdf3b606645163bd37d986fd1682ae Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 00:11:27 +0200 Subject: [PATCH 41/61] Remove unused attribute --- pytest_bdd/parser.py | 1 - pytest_bdd/scenario.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index ad6b89db4..f2a332688 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -296,7 +296,6 @@ def __init__(self, feature: Feature, name: str, line_number: int, steps: "typing self.line_number = line_number self.tags = tags or set() self.failed = False - self.test_function = None class Step: diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index fc0c6e6ba..e5b6d0e2d 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -188,7 +188,6 @@ def scenario_wrapper(request, _pytest_bdd_example): scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}" scenario_wrapper.__scenario__ = templated_scenario - scenario.test_function = scenario_wrapper # TODO: Check usages and remove return scenario_wrapper return decorator From dfe124b86d038db8db3e0627c07020905cb56d1a Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 00:12:49 +0200 Subject: [PATCH 42/61] Sort imports --- pytest_bdd/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytest_bdd/utils.py b/pytest_bdd/utils.py index 6722d270c..272a5fecd 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -2,10 +2,9 @@ import base64 import pickle import re -from inspect import getframeinfo -from inspect import signature -from sys import _getframe import typing +from inspect import getframeinfo, signature +from sys import _getframe if typing.TYPE_CHECKING: from _pytest.pytester import RunResult From a1c92bdbe142db10deb1f886d34d21ea2d7f0acd Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 00:19:24 +0200 Subject: [PATCH 43/61] Add isort configuration and pre-commit hook --- .pre-commit-config.yaml | 9 +++++++-- pyproject.toml | 5 +++++ pytest_bdd/__init__.py | 2 +- pytest_bdd/feature.py | 2 +- pytest_bdd/parser.py | 2 +- pytest_bdd/plugin.py | 6 +----- pytest_bdd/scenario.py | 3 ++- pytest_bdd/steps.py | 3 +-- tests/feature/test_background.py | 1 - tests/feature/test_gherkin_terminal_reporter.py | 2 +- tests/steps/test_steps.py | 3 ++- 11 files changed, 22 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a93974b25..6db118d5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,9 +2,14 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black - rev: 21.6b0 + rev: 21.9b0 hooks: - id: black +- repo: https://github.com/pycqa/isort + rev: 5.9.3 + hooks: + - id: isort + name: isort (python) - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: @@ -13,7 +18,7 @@ repos: - id: check-yaml - id: check-added-large-files - repo: https://github.com/asottile/pyupgrade - rev: v2.19.4 + rev: v2.26.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/pyproject.toml b/pyproject.toml index 7c47b9d0f..cc1aff8d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,8 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 120 target-version = ['py36', 'py37', 'py38'] + +[tool.isort] +profile = "black" +line_length = 120 +multi_line_output = 3 diff --git a/pytest_bdd/__init__.py b/pytest_bdd/__init__.py index 1bf999917..cb4bc1f58 100644 --- a/pytest_bdd/__init__.py +++ b/pytest_bdd/__init__.py @@ -1,7 +1,7 @@ """pytest-bdd public API.""" -from pytest_bdd.steps import given, when, then from pytest_bdd.scenario import scenario, scenarios +from pytest_bdd.steps import given, then, when __version__ = "4.1.0" diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index e14e69feb..e32980bf3 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -28,7 +28,7 @@ import glob2 -from .parser import parse_feature, Feature +from .parser import Feature, parse_feature # Global features dictionary features = {} diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index f2a332688..5dd8fb4ad 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -4,7 +4,7 @@ import typing from collections import OrderedDict -from . import types, exceptions +from . import exceptions, types SPLIT_LINE_RE = re.compile(r"(?") diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index e82a2b1c3..c6d42e8f9 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -2,11 +2,7 @@ import pytest -from . import cucumber_json -from . import generation -from . import gherkin_terminal_reporter -from . import given, when, then -from . import reporting +from . import cucumber_json, generation, gherkin_terminal_reporter, given, reporting, then, when from .utils import CONFIG_STACK diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index e5b6d0e2d..3993931c2 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -24,9 +24,10 @@ from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path if typing.TYPE_CHECKING: - from .parser import ScenarioTemplate, Scenario, Feature from _pytest.mark.structures import ParameterSet + from .parser import Feature, Scenario, ScenarioTemplate + PYTHON_REPLACE_REGEX = re.compile(r"\W") ALPHA_REGEX = re.compile(r"^\d+_*") diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index df6e9543a..509ef83c3 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -36,11 +36,10 @@ def given_beautiful_article(article): """ import pytest - from _pytest.fixtures import FixtureDef -from .types import GIVEN, WHEN, THEN from .parsers import get_parser +from .types import GIVEN, THEN, WHEN from .utils import get_caller_module_locals diff --git a/tests/feature/test_background.py b/tests/feature/test_background.py index c2a88c6e9..a2477840f 100644 --- a/tests/feature/test_background.py +++ b/tests/feature/test_background.py @@ -2,7 +2,6 @@ import textwrap - FEATURE = """\ Feature: Background support diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py index 84a4a3912..52946bfe9 100644 --- a/tests/feature/test_gherkin_terminal_reporter.py +++ b/tests/feature/test_gherkin_terminal_reporter.py @@ -1,6 +1,6 @@ import textwrap -import pytest +import pytest FEATURE = """\ Feature: Gherkin terminal output feature diff --git a/tests/steps/test_steps.py b/tests/steps/test_steps.py index a7eded740..f08be0cb6 100644 --- a/tests/steps/test_steps.py +++ b/tests/steps/test_steps.py @@ -1,8 +1,9 @@ """Test when and then steps are callables.""" -import pytest import textwrap +import pytest + def test_when_then(testdir): """Test when and then steps are callable functions. From bf85037f56be0ca7996af2fbee640714beb452e8 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 00:21:30 +0200 Subject: [PATCH 44/61] Fix imports --- tests/feature/test_no_sctrict_gherkin.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/feature/test_no_sctrict_gherkin.py b/tests/feature/test_no_sctrict_gherkin.py index 5e8d4ebd9..2fc41abe4 100644 --- a/tests/feature/test_no_sctrict_gherkin.py +++ b/tests/feature/test_no_sctrict_gherkin.py @@ -7,10 +7,7 @@ def test_background_no_strict_gherkin(testdir): test_gherkin=""" import pytest - from pytest_bdd import ( - when, - scenario, - ) + from pytest_bdd import when, scenario @scenario( "no_strict_gherkin_background.feature", @@ -66,10 +63,7 @@ def test_scenario_no_strict_gherkin(testdir): test_gherkin=""" import pytest - from pytest_bdd import ( - when, - scenario, - ) + from pytest_bdd import when, scenario @scenario( "no_strict_gherkin_scenario.feature", From 89ef9e0f62b7c0878d2bd38b678bd761db6beb71 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 12:56:04 +0200 Subject: [PATCH 45/61] remove copy-pasted code --- pytest_bdd/parser.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 5dd8fb4ad..1ecb24546 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -245,11 +245,7 @@ def steps(self): background = self.feature.background return (background.steps if background else []) + self._steps - def render(self, context) -> "Scenario": - background = self.feature.background - - templated_steps = (background.steps if background else []) + self._steps - + def render(self, context: typing.Mapping[str, typing.Any]) -> "Scenario": steps = [ Step( name=templated_step.render(context), @@ -258,7 +254,7 @@ def render(self, context) -> "Scenario": line_number=templated_step.line_number, keyword=templated_step.keyword, ) - for templated_step in templated_steps + for templated_step in self.steps ] return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags) From e6aefa55afb21c051660a7fa01b4b576ea600126 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 13:19:01 +0200 Subject: [PATCH 46/61] Fix types --- pytest_bdd/feature.py | 2 +- pytest_bdd/parser.py | 6 +++--- pytest_bdd/scenario.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index e32980bf3..e4f970bcb 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -31,7 +31,7 @@ from .parser import Feature, parse_feature # Global features dictionary -features = {} +features: typing.Dict[str, Feature] = {} def get_feature(base_path, filename, encoding="utf-8") -> Feature: diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 1ecb24546..8cbfbb145 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -74,7 +74,7 @@ def get_step_type(line): return _type -def parse_feature(basedir, filename, encoding="utf-8"): +def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> "Feature": """Parse the feature file. :param str basedir: Feature files base directory. @@ -97,7 +97,7 @@ def parse_feature(basedir, filename, encoding="utf-8"): scenario: typing.Optional[ScenarioTemplate] = None mode = None prev_mode = None - description = [] + description: typing.List[str] = [] step = None multiline_step = False prev_line = None @@ -202,7 +202,7 @@ class Feature: """Feature.""" def __init__(self, scenarios, filename, rel_filename, name, tags, examples, background, line_number, description): - self.scenarios: typing.Mapping[str, ScenarioTemplate] = scenarios + self.scenarios: typing.Dict[str, ScenarioTemplate] = scenarios self.rel_filename = rel_filename self.filename = filename self.tags = tags diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 3993931c2..c9446e38e 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -211,7 +211,7 @@ def collect_example_parametrizations( for scenario_context in scenario_contexts or [{}] ] if contexts == [{}]: - return + return None return [pytest.param(context, id="-".join(context.values())) for context in contexts] From 764bf79e8c961d128b9654e0cbb968dcf99df014 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 13:29:07 +0200 Subject: [PATCH 47/61] Fix TODOs --- tests/feature/test_outline.py | 1 - tests/feature/test_parametrized.py | 34 ++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index ae1844170..10dd28aca 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -353,7 +353,6 @@ def eat_fruits(start_fruits, eat, fruits): @then(parsers.parse("I should have {left} {fruits}")) def should_have_left_fruits(start_fruits, start, eat, left, fruits): - # TODO: it seems that the "start" fixture is injected, but it should not exists! left = int(left) dump_obj(left, fruits) assert left == start_fruits[fruits]["start"] - start_fruits[fruits]["eat"] diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index 26b655df1..7c766b42e 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -1,8 +1,8 @@ import textwrap +from pytest_bdd.utils import collect_dumped_objects + -# TODO: This test was testing a behaviour that is different now. Do we want to support it? -# I think not, but not sure def test_parametrized(testdir): """Test parametrized scenario.""" testdir.makefile( @@ -23,13 +23,9 @@ def test_parametrized(testdir): """\ import pytest from pytest_bdd import given, when, then, scenario, parsers + from pytest_bdd.utils import dump_obj - @pytest.mark.parametrize(["start", "eat", "left"], [(12, 5, 7)]) - @scenario("parametrized.feature", "Parametrized given, when, thens") - def test_parametrized(request, start, eat, left): - pass - @pytest.fixture(params=[1, 2]) def foo_bar(request): return "bar" * request.param @@ -37,27 +33,47 @@ def foo_bar(request): @pytest.mark.parametrize(["start", "eat", "left"], [(12, 5, 7)]) @scenario("parametrized.feature", "Parametrized given, when, thens") + def test_parametrized(request, start, eat, left): + pass + + + @pytest.mark.parametrize(["start", "eat", "left"], [(2, 1, 1)]) + @scenario("parametrized.feature", "Parametrized given, when, thens") def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar): pass + @given(parsers.parse("there are {start} cucumbers"), target_fixture="start_cucumbers") def start_cucumbers(start): + dump_obj(start) return dict(start=start) @when(parsers.parse("I eat {eat} cucumbers")) def eat_cucumbers(start_cucumbers, start, eat): + dump_obj(eat) start_cucumbers["eat"] = eat @then(parsers.parse("I should have {left} cucumbers")) def should_have_left_cucumbers(start_cucumbers, start, eat, left): + dump_obj(left) assert start - eat == left - assert int(left) == start_cucumbers["start"] - start_cucumbers["eat"] + assert start_cucumbers["start"] == start + assert start_cucumbers["eat"] == eat """ ) ) result = testdir.runpytest("-s") result.assert_outcomes(passed=3) - # TODO: We should test the parametrization of each test item, otherwise it's quite useless + + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 12, 5, 7, + # The second test uses is duplicated because of the `foo_bar` indirect fixture + 2, 1, 1, + 2, 1, 1, + ] + # fmt: on From 918fecf7b423f219d5df80d8d5f172cff55dd742 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 13:31:04 +0200 Subject: [PATCH 48/61] Simplify test --- tests/feature/test_report.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 13b6705ac..5f888ec9a 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -76,19 +76,19 @@ def some_other_passing_step(): def a_failing_step(): raise Exception('Error') - @given(parsers.parse('there are {start} cucumbers'), target_fixture="start_cucumbers") + @given(parsers.parse('there are {start:d} cucumbers'), target_fixture="start_cucumbers") def start_cucumbers(start): - return dict(start=int(start)) + return dict(start=start) - @when(parsers.parse('I eat {eat} cucumbers')) + @when(parsers.parse('I eat {eat:g} cucumbers')) def eat_cucumbers(start_cucumbers, eat): - start_cucumbers['eat'] = float(eat) + start_cucumbers['eat'] = eat - @then(parsers.parse('I should have {left} cucumbers')) + @then(parsers.parse('I should have {left:d} cucumbers')) def should_have_left_cucumbers(start_cucumbers, left): - assert start_cucumbers['start'] - start_cucumbers['eat'] == int(left) + assert start_cucumbers['start'] - start_cucumbers['eat'] == left scenarios('test.feature') From e0ce7e46f6cce7c4590381e87955127fd2055b60 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 13:35:46 +0200 Subject: [PATCH 49/61] FIx typing --- pytest_bdd/feature.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index e4f970bcb..0cdc36cff 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -20,7 +20,7 @@ And the article should be published # Note: will query the database :note: The "#" symbol is used for comments. -:note: There're no multiline steps, the description of the step must fit in +:note: There are no multiline steps, the description of the step must fit in one line. """ import os.path @@ -34,7 +34,7 @@ features: typing.Dict[str, Feature] = {} -def get_feature(base_path, filename, encoding="utf-8") -> Feature: +def get_feature(base_path: str, filename: str, encoding: str = "utf-8") -> Feature: """Get a feature by the filename. :param str base_path: Base feature directory. @@ -56,7 +56,7 @@ def get_feature(base_path, filename, encoding="utf-8") -> Feature: return feature -def get_features(paths, **kwargs) -> typing.List[Feature]: +def get_features(paths: typing.List[str], **kwargs) -> typing.List[Feature]: """Get features for given paths. :param list paths: `list` of paths (file or dirs) From 58cf81dc79f495c89bda88d76e5a0b576107b69e Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 13:39:04 +0200 Subject: [PATCH 50/61] Remove TODO --- tests/feature/test_outline_empty_values.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/feature/test_outline_empty_values.py b/tests/feature/test_outline_empty_values.py index 5cdbbff31..05240b8fb 100644 --- a/tests/feature/test_outline_empty_values.py +++ b/tests/feature/test_outline_empty_values.py @@ -53,12 +53,6 @@ def test_scenario_with_empty_example_values(testdir): import json @scenario("outline.feature", "Outlined with empty example values") - # TODO: It seems that we can't have: - # def test_outline(_pytest_bdd_example, start_cucumbers, eat_cucumbers, left_cucumbers): - # This can be quite a problem for old usages. Think about what to do. - # Maybe just mention this in the release notes too. And mention that the `target_fixtures` can be accessed - # by the scenario test function using ``request.getfixturevalue(...)`` - # The scenario test function should be not used anyway. def test_outline(): pass """ From a4c27cf6ba20258a12bbf78f3413edf28debbd68 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 18:31:35 +0200 Subject: [PATCH 51/61] Update changelog --- CHANGES.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b40f93df8..1c90f501b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,10 +5,10 @@ Unreleased ----------- This release introduces breaking changes, please refer to the :ref:`Migration from 4.x.x`. -- Examples are now always expanded in the scenarios. -- Removed ``example_converters`` from ``scenario(...)`` signature -- Removed ``--cucumberjson-expanded`` and ``--cucumber-json-expanded`` options. Now the JSON report is always expanded -- Removed ``--gherkin-terminal-reporter-expanded`` option. Now the terminal report is always expanded +- Rewrite the logic to parse Examples for Scenario Outlines. Now the substitution of the examples is done during the parsing of Gherkin feature files. You won't need to define the steps twice like ``@given("there are cucumbers")`` and ``@given(parsers.parse("there are {start} cucumbers"))``. The latter will be enough. +- Removed ``example_converters`` from ``scenario(...)`` signature. You should now use just the ``converters`` parameter for ``given``, ``when``, ``then``. +- Removed ``--cucumberjson-expanded`` and ``--cucumber-json-expanded`` options. Now the JSON report is always expanded. +- Removed ``--gherkin-terminal-reporter-expanded`` option. Now the terminal report is always expanded. 4.1.0 ----------- From 99420f2c7f51f1441a96202de6e4c48b3dd4e3fc Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 18:48:20 +0200 Subject: [PATCH 52/61] Update Readme --- README.rst | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 1e60080d4..1a19c7d4f 100644 --- a/README.rst +++ b/README.rst @@ -111,8 +111,6 @@ Scenario decorator Functions decorated with the `scenario` decorator behave like a normal test function, and they will be executed after all scenario steps. -You can consider it as a normal pytest test function, e.g. order fixtures there, -call other functions and make assertions: .. code-block:: python @@ -124,6 +122,9 @@ call other functions and make assertions: assert article.title in browser.html +.. NOTE:: It is however encouraged to try as much as possible to have your logic only inside the Given, When, Then steps. + + Step aliases ------------ @@ -287,7 +288,7 @@ You can implement your own step parser. It's interface is quite simple. The code def __init__(self, name, **kwargs): """Compile regex.""" - super(re, self).__init__(name) + super().__init__(name) self.regex = re.compile(re.sub("%(.+)%", "(?P<\1>.+)", self.name), **kwargs) def parse_arguments(self, name): @@ -1207,23 +1208,43 @@ ordering of the types of the steps. Migration of your tests from versions 4.x.x ------------------------------------------- -The templated steps should use step argument parsers in order to match the scenario outlines -and get the values from the example tables. The values from the example tables are no longer -passed as fixtures. +The templated steps (e.g. ``@given("there are cucumbers")``) should use step argument parsers in order to match the scenario outlines and get the values from the example tables. The values from the example tables are no longer passed as fixtures per se, although if you define your step to use a parser, the parameters will be still provided as fixtures. + +.. code-block:: python + + # Old step definition: + @given("there are cucumbers") + def given_cucumbers(start): + pass + + # New step definition: + @given(parsers.parse("there are {start} cucumbers")) + def given_cucumbers(start): + pass + + +Scenario `example_converters` are removed in favor of the converters provided on the step level: .. code-block:: python - # Instead of - # @given("there are in the box") - # def given_cucumbers(cucumbers): - # pass + # Old code: + @given("there are cucumbers") + def given_cucumbers(start): + return {"start": start} - @given(parsers.parse("there are {cucumbers} in the box")) - def given_cucumbers(cucumbers): + @scenario("outline.feature", "Outlined", example_converters={"start": float}) + def test_outline(): pass + # New code: + @given(parsers.parse("there are {start} cucumbers"), converters={"start": float}) + def given_cucumbers(start): + return {"start": start} + + @scenario("outline.feature", "Outlined") + def test_outline(): + pass -Scenario `example_converters` are removed in favor of the converters provided on the step level. .. _Migration from 3.x.x: From 9383530c0402b571d09c8e998ce625b79c88c9e7 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 19:01:23 +0200 Subject: [PATCH 53/61] Update README (mainly fix typos, remove outdated options) --- README.rst | 51 +++++++++++++++++++-------------------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/README.rst b/README.rst index 1a19c7d4f..afe1bf87e 100644 --- a/README.rst +++ b/README.rst @@ -235,7 +235,7 @@ Example: .. code-block:: gherkin Feature: Step arguments - Scenario: Arguments for given, when, thens + Scenario: Arguments for given, when, then Given there are 5 cucumbers When I eat 3 cucumbers @@ -252,7 +252,7 @@ The code will look like: from pytest_bdd import scenario, given, when, then, parsers - @scenario("arguments.feature", "Arguments for given, when, thens") + @scenario("arguments.feature", "Arguments for given, when, then") def test_arguments(): pass @@ -312,9 +312,9 @@ Step arguments are fixtures as well! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Step arguments are injected into pytest `request` context as normal fixtures with the names equal to the names of the -arguments. This opens a number of possibilies: +arguments. This opens a number of possibilities: -* you can access step's argument as a fixture in other step function just by mentioning it as an argument (just like any othe pytest fixture) +* you can access step's argument as a fixture in other step function just by mentioning it as an argument (just like any other pytest fixture) * if the name of the step argument clashes with existing fixture, it will be overridden by step's argument value; this way you can set/override the value for some fixture deeply inside of the fixture tree in a ad-hoc way by just choosing the proper name for the step argument. @@ -450,7 +450,7 @@ step arguments and capture lines after first line (or some subset of them) into assert i_have_text == text == 'Some\nExtra\nLines' Note that `then` step definition (`text_should_be_correct`) in this example uses `text` fixture which is provided -by a a `given` step (`i_have_text`) argument with the same name (`text`). This possibility is described in +by a `given` step (`i_have_text`) argument with the same name (`text`). This possibility is described in the `Step arguments are fixtures as well!`_ section. @@ -504,7 +504,7 @@ Scenario outlines ----------------- Scenarios can be parametrized to cover few cases. In Gherkin the variable -templates are written using corner braces as . +templates are written using corner braces as ````. `Gherkin scenario outlines `_ are supported by pytest-bdd exactly as it's described in be behave_ docs. @@ -513,7 +513,7 @@ Example: .. code-block:: gherkin Feature: Scenario outlines - Scenario Outline: Outlined given, when, thens + Scenario Outline: Outlined given, when, then Given there are cucumbers When I eat cucumbers Then I should have cucumbers @@ -528,7 +528,7 @@ pytest-bdd feature file format also supports example tables in different way: .. code-block:: gherkin Feature: Scenario outlines - Scenario Outline: Outlined given, when, thens + Scenario Outline: Outlined given, when, then Given there are cucumbers When I eat cucumbers Then I should have cucumbers @@ -550,7 +550,7 @@ The code will look like: @scenario( "outline.feature", - "Outlined given, when, thens", + "Outlined given, when, then", ) def test_outlined(): pass @@ -659,7 +659,7 @@ The code will look like: ) @scenario( "parametrized.feature", - "Parametrized given, when, thens", + "Parametrized given, when, then", ) # Note that we should take the same arguments in the test function that we use # for the test parametrization either directly or indirectly (fixtures depend on them). @@ -689,7 +689,7 @@ With a parametrized.feature file: .. code-block:: gherkin Feature: parametrized - Scenario: Parametrized given, when, thens + Scenario: Parametrized given, when, then Given there are cucumbers When I eat cucumbers Then I should have cucumbers @@ -768,12 +768,12 @@ scenario test, so we can use standard test selection: pytest -m "backend and login and successful" -The feature and scenario markers are not different from standard pytest markers, and the `@` symbol is stripped out +The feature and scenario markers are not different from standard pytest markers, and the ``@`` symbol is stripped out automatically to allow test selector expressions. If you want to have bdd-related tags to be distinguishable from the other test markers, use prefix like `bdd`. Note that if you use pytest `--strict` option, all bdd tags mentioned in the feature files should be also in the -`markers` setting of the `pytest.ini` config. Also for tags please use names which are python-compartible variable -names, eg starts with a non-number, underscore alphanumberic, etc. That way you can safely use tags for tests filtering. +`markers` setting of the `pytest.ini` config. Also for tags please use names which are python-compatible variable +names, eg starts with a non-number, underscore alphanumeric, etc. That way you can safely use tags for tests filtering. You can customize how tags are converted to pytest marks by implementing the ``pytest_bdd_apply_tag`` hook and returning ``True`` from it: @@ -786,7 +786,7 @@ You can customize how tags are converted to pytest marks by implementing the marker(function) return True else: - # Fall back to pytest-bdd's default behavior + # Fall back to the default behavior of pytest-bdd return None Test setup @@ -973,7 +973,7 @@ test_common.py: pass There are no definitions of the steps in the test file. They were -collected from the parent conftests. +collected from the parent conftest.py. Using unicode in the feature files @@ -1045,7 +1045,7 @@ The `features_base_dir` parameter can also be passed to the `@scenario` decorato Avoid retyping the feature file name ------------------------------------ -If you want to avoid retyping the feature file name when defining your scenarios in a test file, use functools.partial. +If you want to avoid retyping the feature file name when defining your scenarios in a test file, use ``functools.partial``. This will make your life much easier when defining multiple scenarios in a test file. For example: test_publish_article.py: @@ -1113,8 +1113,8 @@ Reporting It's important to have nice reporting out of your bdd tests. Cucumber introduced some kind of standard for `json format `_ -which can be used for `this `_ jenkins -plugin +which can be used for, for example, by `this `_ Jenkins +plugin. To have an output in json format: @@ -1123,11 +1123,6 @@ To have an output in json format: pytest --cucumberjson= This will output an expanded (meaning scenario outlines will be expanded to several scenarios) cucumber format. -To also fill in parameters in the step name, you have to explicitly tell pytest-bdd to use the expanded format: - -:: - - pytest --cucumberjson= --cucumberjson-expanded To enable gherkin-formatted output on terminal, use @@ -1136,14 +1131,6 @@ To enable gherkin-formatted output on terminal, use pytest --gherkin-terminal-reporter -Terminal reporter supports expanded format as well - -:: - - pytest --gherkin-terminal-reporter-expanded - - - Test code generation helpers ---------------------------- From b817fbf9435ef1cbcc007f13a46813e8a502f48f Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 19:06:04 +0200 Subject: [PATCH 54/61] Fix examples in README --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index afe1bf87e..3ffdcb773 100644 --- a/README.rst +++ b/README.rst @@ -429,7 +429,7 @@ step arguments and capture lines after first line (or some subset of them) into import re - from pytest_bdd import given, then, scenario + from pytest_bdd import given, then, scenario, parsers @scenario( @@ -545,7 +545,7 @@ The code will look like: .. code-block:: python - from pytest_bdd import given, when, then, scenario + from pytest_bdd import given, when, then, scenario, parsers @scenario( @@ -649,7 +649,7 @@ The code will look like: .. code-block:: python import pytest - from pytest_bdd import scenario, given, when, then + from pytest_bdd import scenario, given, when, then, parsers # Here we use pytest to parametrize the test with the parameters table From e2eadb5c8ed8d1dec1a228787cdd85707f299615 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 19:06:16 +0200 Subject: [PATCH 55/61] Remove python2 junk --- README.rst | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/README.rst b/README.rst index 3ffdcb773..615babeb4 100644 --- a/README.rst +++ b/README.rst @@ -976,22 +976,6 @@ There are no definitions of the steps in the test file. They were collected from the parent conftest.py. -Using unicode in the feature files ----------------------------------- - -As mentioned above, by default, utf-8 encoding is used for parsing feature files. -For steps definition, you should use unicode strings, which is the default in python 3. -If you are on python 2, make sure you use unicode strings by prefixing them with the `u` sign. - - -.. code-block:: python - - @given(parsers.re(u"у мене є рядок який містить '{0}'".format(u'(?P.+)'))) - def there_is_a_string_with_content(content, string): - """Create string with unicode content.""" - string["content"] = content - - Default steps ------------- From 030937f62585c5b1915fef47aebc53aa8484f689 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 19:08:44 +0200 Subject: [PATCH 56/61] Fix migration guide --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 615babeb4..7bf302d5e 100644 --- a/README.rst +++ b/README.rst @@ -1179,7 +1179,7 @@ ordering of the types of the steps. Migration of your tests from versions 4.x.x ------------------------------------------- -The templated steps (e.g. ``@given("there are cucumbers")``) should use step argument parsers in order to match the scenario outlines and get the values from the example tables. The values from the example tables are no longer passed as fixtures per se, although if you define your step to use a parser, the parameters will be still provided as fixtures. +Templated steps (e.g. ``@given("there are cucumbers")``) should now the use step argument parsers in order to match the scenario outlines and get the values from the example tables. The values from the example tables are no longer passed as fixtures, although if you define your step to use a parser, the parameters will be still provided as fixtures. .. code-block:: python @@ -1188,6 +1188,7 @@ The templated steps (e.g. ``@given("there are cucumbers")``) should use def given_cucumbers(start): pass + # New step definition: @given(parsers.parse("there are {start} cucumbers")) def given_cucumbers(start): @@ -1207,6 +1208,7 @@ Scenario `example_converters` are removed in favor of the converters provided on def test_outline(): pass + # New code: @given(parsers.parse("there are {start} cucumbers"), converters={"start": float}) def given_cucumbers(start): From 02ce7171e21a7f6a2c9b7a6c2dec47047285ea6f Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 19:40:05 +0200 Subject: [PATCH 57/61] Fix migration guide --- tests/feature/test_outline.py | 64 +++++++++++++++++------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 10dd28aca..58e2e9acd 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -9,28 +9,27 @@ from pytest_bdd.utils import dump_obj -@given(parsers.parse("there are {start} cucumbers"), target_fixture="start_cucumbers") +@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="start_cucumbers") def start_cucumbers(start): - assert isinstance(start, str) - start = int(start) + assert isinstance(start, int) dump_obj(start) - return dict(start=start) + return {"start": start} -@when(parsers.parse("I eat {eat} cucumbers")) +@when(parsers.parse("I eat {eat:g} cucumbers")) def eat_cucumbers(start_cucumbers, eat): - assert isinstance(eat, str) - eat = int(eat) + assert isinstance(eat, float) dump_obj(eat) start_cucumbers["eat"] = eat @then(parsers.parse("I should have {left} cucumbers")) -def should_have_left_cucumbers(start_cucumbers, left): +def should_have_left_cucumbers(start_cucumbers, start, eat, left): assert isinstance(left, str) - left = int(left) dump_obj(left) - assert left == start_cucumbers["start"] - start_cucumbers["eat"] + assert start - eat == int(left) + assert start_cucumbers["start"] == start + assert start_cucumbers["eat"] == eat """ @@ -50,7 +49,6 @@ def test_outlined(testdir): | start | eat | left | | 12 | 5 | 7 | # a comment | 5 | 4 | 1 | - | 5 | 4 | 43 | # Control case. This should fail. """ ), @@ -74,7 +72,7 @@ def test_outline(request): ) ) result = testdir.runpytest() - result.assert_outcomes(passed=2, failed=1) + result.assert_outcomes(passed=2) def test_wrongly_outlined(testdir): @@ -261,11 +259,9 @@ def test_vertical_example(testdir): Then I should have cucumbers Examples: Vertical - | start | 12 | 2 | 2 | - | eat | 5 | 1 | 1 | - | left | 7 | 1 | 42 | - - # The last column is the control case, to verify that the scenario fails + | start | 12 | 2 | + | eat | 5 | 1 | + | left | 7 | 1 | """ ), @@ -288,13 +284,12 @@ def test_outline(): ) ) result = testdir.runpytest("-s") - result.assert_outcomes(passed=2, failed=1) + result.assert_outcomes(passed=2) parametrizations = collect_dumped_objects(result) # fmt: off assert parametrizations == [ - 12, 5, 7, - 2, 1, 1, - 2, 1, 42, + 12, 5.0, "7", + 2, 1.0, "1", ] # fmt: on @@ -337,25 +332,30 @@ def test_outlined_feature(testdir): def test_outline(): pass - @given(parsers.parse("there are {start} {fruits}"), target_fixture="start_fruits") + @given(parsers.parse("there are {start:d} {fruits}"), target_fixture="start_fruits") def start_fruits(start, fruits): - start = int(start) dump_obj(start, fruits) - return {fruits: {"start": start}} + assert isinstance(start, int) + return {fruits: dict(start=start)} - @when(parsers.parse("I eat {eat} {fruits}")) + + @when(parsers.parse("I eat {eat:g} {fruits}")) def eat_fruits(start_fruits, eat, fruits): - eat = float(eat) dump_obj(eat, fruits) + + assert isinstance(eat, float) start_fruits[fruits]["eat"] = eat @then(parsers.parse("I should have {left} {fruits}")) def should_have_left_fruits(start_fruits, start, eat, left, fruits): - left = int(left) dump_obj(left, fruits) - assert left == start_fruits[fruits]["start"] - start_fruits[fruits]["eat"] + + assert isinstance(left, str) + assert start - eat == int(left) + assert start_fruits[fruits]["start"] == start + assert start_fruits[fruits]["eat"] == eat """ ) @@ -365,10 +365,10 @@ def should_have_left_fruits(start_fruits, start, eat, left, fruits): parametrizations = collect_dumped_objects(result) # fmt: off assert parametrizations == [ - 12, "oranges", 5, "oranges", 7, "oranges", - 12, "apples", 5, "apples", 7, "apples", - 5, "oranges", 4, "oranges", 1, "oranges", - 5, "apples", 4, "apples", 1, "apples", + 12, "oranges", 5.0, "oranges", "7", "oranges", + 12, "apples", 5.0, "apples", "7", "apples", + 5, "oranges", 4.0, "oranges", "1", "oranges", + 5, "apples", 4.0, "apples", "1", "apples", ] # fmt: on From 2710c5aaccb553451c92955bf316fba8ed5ac91c Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 19:43:14 +0200 Subject: [PATCH 58/61] Improve test assertion --- tests/feature/test_outline.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 58e2e9acd..3628db8d7 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -71,8 +71,14 @@ def test_outline(request): """ ) ) - result = testdir.runpytest() + result = testdir.runpytest("-s") result.assert_outcomes(passed=2) + # fmt: off + assert collect_dumped_objects(result) == [ + 12, 5.0, "7", + 5, 4.0, "1", + ] + # fmt: on def test_wrongly_outlined(testdir): From d5df79e42ce0660e3d3d265032478660430e5b65 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 19:44:40 +0200 Subject: [PATCH 59/61] Simplify test --- tests/feature/test_outline_empty_values.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/feature/test_outline_empty_values.py b/tests/feature/test_outline_empty_values.py index 05240b8fb..de944211a 100644 --- a/tests/feature/test_outline_empty_values.py +++ b/tests/feature/test_outline_empty_values.py @@ -60,10 +60,7 @@ def test_outline(): ) result = testdir.runpytest("-s") result.assert_outcomes(passed=1) - [start, eat, left] = collect_dumped_objects(result) - assert start == "#" - assert eat == "" - assert left == "" + assert collect_dumped_objects(result) == ["#", "", ""] def test_scenario_with_empty_example_values_vertical(testdir): @@ -99,8 +96,5 @@ def test_outline(): ) ) result = testdir.runpytest("-s") - [start, eat, left] = collect_dumped_objects(result) - assert start == "#" - assert eat == "" - assert left == "" result.assert_outcomes(passed=1) + assert collect_dumped_objects(result) == ["#", "", ""] From 3fe45a88d399efed8d0af173b2405e88e7b4814e Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 19:48:02 +0200 Subject: [PATCH 60/61] Fix test to reflect how it was before --- tests/feature/test_report.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 5f888ec9a..f8e6f8d28 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -78,17 +78,22 @@ def a_failing_step(): @given(parsers.parse('there are {start:d} cucumbers'), target_fixture="start_cucumbers") def start_cucumbers(start): - return dict(start=start) + assert isinstance(start, int) + return {"start": start} @when(parsers.parse('I eat {eat:g} cucumbers')) def eat_cucumbers(start_cucumbers, eat): + assert isinstance(eat, float) start_cucumbers['eat'] = eat - @then(parsers.parse('I should have {left:d} cucumbers')) - def should_have_left_cucumbers(start_cucumbers, left): - assert start_cucumbers['start'] - start_cucumbers['eat'] == left + @then(parsers.parse('I should have {left} cucumbers')) + def should_have_left_cucumbers(start_cucumbers, start, eat, left): + assert isinstance(left, str) + assert start - eat == int(left) + assert start_cucumbers['start'] == start + assert start_cucumbers['eat'] == eat scenarios('test.feature') From 28f3261476a1479c9d36a4124dee7eb57f082a8a Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Sun, 19 Sep 2021 19:50:08 +0200 Subject: [PATCH 61/61] use converters --- tests/feature/test_report.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index f8e6f8d28..f11f8d0f5 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -297,9 +297,14 @@ def parse(cls, value): class Alien(object): pass - @given(parsers.parse('there is a coordinate {point}'), target_fixture="point") + @given( + parsers.parse('there is a coordinate {point}'), + target_fixture="point", + converters={"point": Point.parse}, + ) def given_there_is_a_point(point): - return Point.parse(point) + assert isinstance(point, Point) + return point @pytest.mark.parametrize('alien', [Alien()])