From 014ec83732aeecc7957ea743c9203f079ba36e2e Mon Sep 17 00:00:00 2001 From: Ludovic CH Date: Wed, 2 Apr 2025 16:14:51 +0200 Subject: [PATCH 1/5] feat: add examples tag to scenario tags --- src/pytest_bdd/parser.py | 10 +- tests/parser/test.feature | 33 +++- tests/parser/test_parser.py | 323 +++++++++++++++++++++++++++++++----- 3 files changed, 316 insertions(+), 50 deletions(-) diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index e2be8482..2c45481f 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -208,6 +208,7 @@ def render(self, context: Mapping[str, object]) -> Scenario: Returns: Scenario: A Scenario object with steps rendered based on the context. """ + example = self.find_scenario_example_from_context(context) base_steps = self.all_background_steps + self._steps scenario_steps = [ Step( @@ -227,11 +228,18 @@ def render(self, context: Mapping[str, object]) -> Scenario: name=render_string(self.name, context), line_number=self.line_number, steps=scenario_steps, - tags=self.tags, + tags=self.tags | example.tags if example else self.tags, description=self.description, rule=self.rule, ) + def find_scenario_example_from_context(self, context: Mapping[str, object]) -> Examples | None: + for example in self.examples: + example_param = dict(zip(example.example_params, example.examples[0])) + if example_param == context: + return example + return None + @dataclass(eq=False) class Scenario: diff --git a/tests/parser/test.feature b/tests/parser/test.feature index 5515bcb1..759329d4 100644 --- a/tests/parser/test.feature +++ b/tests/parser/test.feature @@ -9,7 +9,7 @@ Feature: User login # Background steps run before each scenario Given the login page is open - # Scenario within the rule + # Scenario within the rule Scenario: Successful login with valid credentials Given the user enters a valid username And the user enters a valid password @@ -22,7 +22,7 @@ Feature: User login When the user clicks the login button Then the user should see an error message "" - # Examples table provides data for the scenario outline + # Examples table provides data for the scenario outline Examples: | username | password | error_message | | invalidUser | wrongPass | Invalid username or password | @@ -83,15 +83,32 @@ Feature: User login Please check your username and password and try again. If the problem persists, contact support. """ + # Tags can also be used on exemples + @scenario_tag + Scenario Outline: Test tags on Examples + Given the user enters "" as username + And the user enters "" as password + When the user clicks the login button + Then the user should see an error message "" + + @example_tag_1 + Examples: + | username | password | error_message | + | invalidUser | wrongPass | Invalid username or password | + + @example_tag_2 + Examples: + | username | password | error_message | + | user123 | incorrect | Invalid username or password | @some-tag Rule: a sale cannot happen if there is no stock - # Unhappy path - Example: No chocolates left - Given the customer has 100 cents - And there are no chocolate bars in stock - When the customer tries to buy a 1 cent chocolate bar - Then the sale should not happen + # Unhappy path + Example: No chocolates left + Given the customer has 100 cents + And there are no chocolate bars in stock + When the customer tries to buy a 1 cent chocolate bar + Then the sale should not happen Rule: A sale cannot happen if the customer does not have enough money # Unhappy path diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index 09be7c85..b3327728 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -632,18 +632,252 @@ def test_parser(): examples=[], ), ), + Child( + background=None, + rule=None, + scenario=Scenario( + id="71", + location=Location( + column=3, + line=88, + ), + keyword="Scenario Outline", + name="Test tags on Examples", + description="", + steps=[ + Step( + id="58", + location=Location( + column=5, + line=89, + ), + keyword="Given", + keyword_type="Context", + text='the user enters "" as username', + datatable=None, + docstring=None, + ), + Step( + id="59", + location=Location( + column=5, + line=90, + ), + keyword="And", + keyword_type="Conjunction", + text='the user enters "" as password', + datatable=None, + docstring=None, + ), + Step( + id="60", + location=Location( + column=5, + line=91, + ), + keyword="When", + keyword_type="Action", + text="the user clicks the login button", + datatable=None, + docstring=None, + ), + Step( + id="61", + location=Location( + column=5, + line=92, + ), + keyword="Then", + keyword_type="Outcome", + text='the user should see an error message ""', + datatable=None, + docstring=None, + ), + ], + tags=[ + Tag( + id="70", + location=Location( + column=3, + line=87, + ), + name="@scenario_tag", + ), + ], + examples=[ + ExamplesTable( + location=Location( + column=5, + line=95, + ), + tags=[ + Tag( + id="64", + location=Location( + column=5, + line=94, + ), + name="@example_tag_1", + ), + ], + name="", + table_header=Row( + id="62", + location=Location( + column=7, + line=96, + ), + cells=[ + Cell( + location=Location( + column=9, + line=96, + ), + value="username", + ), + Cell( + location=Location( + column=23, + line=96, + ), + value="password", + ), + Cell( + location=Location( + column=35, + line=96, + ), + value="error_message", + ), + ], + ), + table_body=[ + Row( + id="63", + location=Location( + column=7, + line=97, + ), + cells=[ + Cell( + location=Location( + column=9, + line=97, + ), + value="invalidUser", + ), + Cell( + location=Location( + column=23, + line=97, + ), + value="wrongPass", + ), + Cell( + location=Location( + column=35, + line=97, + ), + value="Invalid username or password", + ), + ], + ), + ], + ), + ExamplesTable( + location=Location( + column=5, + line=100, + ), + tags=[ + Tag( + id="68", + location=Location( + column=5, + line=99, + ), + name="@example_tag_2", + ), + ], + name="", + table_header=Row( + id="66", + location=Location( + column=7, + line=101, + ), + cells=[ + Cell( + location=Location( + column=9, + line=101, + ), + value="username", + ), + Cell( + location=Location( + column=20, + line=101, + ), + value="password", + ), + Cell( + location=Location( + column=32, + line=101, + ), + value="error_message", + ), + ], + ), + table_body=[ + Row( + id="67", + location=Location( + column=7, + line=102, + ), + cells=[ + Cell( + location=Location( + column=9, + line=102, + ), + value="user123", + ), + Cell( + location=Location( + column=20, + line=102, + ), + value="incorrect", + ), + Cell( + location=Location( + column=32, + line=102, + ), + value="Invalid username or password", + ), + ], + ), + ], + ), + ], + ), + ), Child( background=None, rule=Rule( - id="64", + id="78", keyword="Rule", - location=Location(column=3, line=88), + location=Location(column=3, line=105), name="a sale cannot happen if there is no stock", description="", tags=[ Tag( - id="63", - location=Location(column=3, line=87), + id="77", + location=Location(column=3, line=104), name="@some-tag", ) ], @@ -652,44 +886,44 @@ def test_parser(): background=None, rule=None, scenario=Scenario( - id="62", + id="76", keyword="Example", - location=Location(column=3, line=90), + location=Location(column=5, line=107), name="No chocolates left", description="", steps=[ Step( - id="58", + id="72", keyword="Given", keyword_type="Context", - location=Location(column=5, line=91), + location=Location(column=7, line=108), text="the customer has 100 cents", datatable=None, docstring=None, ), Step( - id="59", + id="73", keyword="And", keyword_type="Conjunction", - location=Location(column=5, line=92), + location=Location(column=7, line=109), text="there are no chocolate bars in stock", datatable=None, docstring=None, ), Step( - id="60", + id="74", keyword="When", keyword_type="Action", - location=Location(column=5, line=93), + location=Location(column=7, line=110), text="the customer tries to buy a 1 cent chocolate bar", datatable=None, docstring=None, ), Step( - id="61", + id="75", keyword="Then", keyword_type="Outcome", - location=Location(column=5, line=94), + location=Location(column=7, line=111), text="the sale should not happen", datatable=None, docstring=None, @@ -706,9 +940,9 @@ def test_parser(): Child( background=None, rule=Rule( - id="75", + id="89", keyword="Rule", - location=Location(column=3, line=96), + location=Location(column=3, line=113), name="A sale cannot happen if the customer does not have enough money", description="", tags=[], @@ -717,44 +951,44 @@ def test_parser(): background=None, rule=None, scenario=Scenario( - id="69", + id="83", keyword="Example", - location=Location(column=5, line=98), + location=Location(column=5, line=115), name="Not enough money", description="", steps=[ Step( - id="65", + id="79", keyword="Given", keyword_type="Context", - location=Location(column=7, line=99), + location=Location(column=7, line=116), text="the customer has 100 cents", datatable=None, docstring=None, ), Step( - id="66", + id="80", keyword="And", keyword_type="Conjunction", - location=Location(column=7, line=100), + location=Location(column=7, line=117), text="there are chocolate bars in stock", datatable=None, docstring=None, ), Step( - id="67", + id="81", keyword="When", keyword_type="Action", - location=Location(column=7, line=101), + location=Location(column=7, line=118), text="the customer tries to buy a 125 cent chocolate bar", datatable=None, docstring=None, ), Step( - id="68", + id="82", keyword="Then", keyword_type="Outcome", - location=Location(column=7, line=102), + location=Location(column=7, line=119), text="the sale should not happen", datatable=None, docstring=None, @@ -768,44 +1002,44 @@ def test_parser(): background=None, rule=None, scenario=Scenario( - id="74", + id="88", keyword="Example", - location=Location(column=5, line=105), + location=Location(column=5, line=122), name="Enough money", description="", steps=[ Step( - id="70", + id="84", keyword="Given", keyword_type="Context", - location=Location(column=7, line=106), + location=Location(column=7, line=123), text="the customer has 100 cents", datatable=None, docstring=None, ), Step( - id="71", + id="85", keyword="And", keyword_type="Conjunction", - location=Location(column=7, line=107), + location=Location(column=7, line=124), text="there are chocolate bars in stock", datatable=None, docstring=None, ), Step( - id="72", + id="86", keyword="When", keyword_type="Action", - location=Location(column=7, line=108), + location=Location(column=7, line=125), text="the customer tries to buy a 75 cent chocolate bar", datatable=None, docstring=None, ), Step( - id="73", + id="87", keyword="Then", keyword_type="Outcome", - location=Location(column=7, line=109), + location=Location(column=7, line=126), text="the sale should happen", datatable=None, docstring=None, @@ -827,10 +1061,10 @@ def test_parser(): location=Location(column=1, line=9), text=" # Background steps run before each scenario", ), - Comment(location=Location(column=1, line=12), text=" # Scenario within the rule"), + Comment(location=Location(column=1, line=12), text=" # Scenario within the rule"), Comment( location=Location(column=1, line=25), - text=" # Examples table provides data for the scenario outline", + text=" # Examples table provides data for the scenario outline", ), Comment( location=Location(column=1, line=54), @@ -844,9 +1078,16 @@ def test_parser(): location=Location(column=1, line=76), text=" # Using Doc Strings for multi-line text", ), - Comment(location=Location(column=1, line=89), text=" # Unhappy path"), - Comment(location=Location(column=1, line=97), text=" # Unhappy path"), - Comment(location=Location(column=1, line=104), text=" # Happy path"), + Comment( + location=Location( + column=1, + line=86, + ), + text=" # Tags can also be used on exemples", + ), + Comment(location=Location(column=1, line=106), text=" # Unhappy path"), + Comment(location=Location(column=1, line=114), text=" # Unhappy path"), + Comment(location=Location(column=1, line=121), text=" # Happy path"), ], ) From a2a00e514c23bca8b912180f4406f6412bcbe572 Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Mon, 14 Apr 2025 13:51:59 +0200 Subject: [PATCH 2/5] tests: add test for parsing --- tests/parser/test_parser.py | 82 ++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index b3327728..40a0dd16 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -1,7 +1,9 @@ from __future__ import annotations +from collections import OrderedDict from pathlib import Path +from pytest_bdd.parser import Examples, Feature, ScenarioTemplate, Step from src.pytest_bdd.gherkin_parser import ( Background, Cell, @@ -10,13 +12,11 @@ DataTable, DocString, ExamplesTable, - Feature, GherkinDocument, Location, Row, Rule, Scenario, - Step, Tag, get_gherkin_document, ) @@ -1092,3 +1092,81 @@ def test_parser(): ) assert gherkin_doc == expected_document + + +def test_render_scenario_with_example_tags(): + # Mock feature and context + feature = Feature( + scenarios=OrderedDict(), + filename="test.feature", + rel_filename="test.feature", + language="en", + keyword="Feature", + name="Test Feature", + tags=set(), + background=None, + line_number=1, + description="A test feature", + ) + context = {"username": "user123", "password": "incorrect", "error_message": "Invalid username or password"} + + # Mock examples with tags + examples = Examples( + line_number=10, + name="Example with tags", + example_params=["username", "password", "error_message"], + examples=[ + ["user123", "incorrect", "Invalid username or password"], + ], + tags={"example_tag_1", "example_tag_2"}, + ) + + # Mock steps + steps = [ + Step( + name="Given the user enters as username", + type="given", + indent=0, + line_number=2, + keyword="Given", + ), + Step( + name="And the user enters as password", + type="and", + indent=0, + line_number=3, + keyword="And", + ), + Step( + name="Then the user should see an error message ", + type="then", + indent=0, + line_number=4, + keyword="Then", + ), + ] + + # Create a ScenarioTemplate + scenario_template = ScenarioTemplate( + feature=feature, + keyword="Scenario Outline", + name="Test Scenario with Example Tags", + line_number=2, + templated=True, + description="A test scenario with example tags", + tags={"scenario_tag"}, + examples=[examples], + ) + for step in steps: + scenario_template.add_step(step) + + # Render the scenario + rendered_scenario = scenario_template.render(context) + + # Assertions + assert rendered_scenario.name == "Test Scenario with Example Tags" + assert len(rendered_scenario.steps) == 3 + assert rendered_scenario.steps[0].name == "Given the user enters user123 as username" + assert rendered_scenario.steps[1].name == "And the user enters incorrect as password" + assert rendered_scenario.steps[2].name == "Then the user should see an error message Invalid username or password" + assert rendered_scenario.tags == {"scenario_tag", "example_tag_1", "example_tag_2"} From 04d1980e05eedc8c0cd4d5d4c56ab6d541ee7dcb Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Tue, 22 Apr 2025 20:56:26 +0200 Subject: [PATCH 3/5] chore: improve imports --- tests/parser/test_parser.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/parser/test_parser.py b/tests/parser/test_parser.py index 40a0dd16..fa5f8fc7 100644 --- a/tests/parser/test_parser.py +++ b/tests/parser/test_parser.py @@ -3,7 +3,6 @@ from collections import OrderedDict from pathlib import Path -from pytest_bdd.parser import Examples, Feature, ScenarioTemplate, Step from src.pytest_bdd.gherkin_parser import ( Background, Cell, @@ -12,14 +11,19 @@ DataTable, DocString, ExamplesTable, + Feature, GherkinDocument, Location, Row, Rule, Scenario, + Step, Tag, get_gherkin_document, ) +from src.pytest_bdd.parser import Examples, ScenarioTemplate +from src.pytest_bdd.parser import Feature as PytestBddFeature +from src.pytest_bdd.parser import Step as PytestBddStep def test_parser(): @@ -1096,7 +1100,7 @@ def test_parser(): def test_render_scenario_with_example_tags(): # Mock feature and context - feature = Feature( + feature = PytestBddFeature( scenarios=OrderedDict(), filename="test.feature", rel_filename="test.feature", @@ -1123,21 +1127,21 @@ def test_render_scenario_with_example_tags(): # Mock steps steps = [ - Step( + PytestBddStep( name="Given the user enters as username", type="given", indent=0, line_number=2, keyword="Given", ), - Step( + PytestBddStep( name="And the user enters as password", type="and", indent=0, line_number=3, keyword="And", ), - Step( + PytestBddStep( name="Then the user should see an error message ", type="then", indent=0, From 61afa2ace3b291ff4224f9ab11115a5696803686 Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Tue, 22 Apr 2025 21:49:50 +0200 Subject: [PATCH 4/5] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c14d755a..25da3147 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytest-bdd" -version = "8.1.0" +version = "8.1.1" description = "BDD for pytest" authors = [ {name="Oleg Pidsadnyi", email="oleg.pidsadnyi@gmail.com"}, From 4abd4672cdd1df73d4c7740febd6e8e3fb8e2cd4 Mon Sep 17 00:00:00 2001 From: Ludovic CHAPELET Date: Tue, 22 Apr 2025 21:57:53 +0200 Subject: [PATCH 5/5] tests: remove test_step_outside_scenario_or_background_error --- tests/parser/test_errors.py | 38 ------------------------------------- 1 file changed, 38 deletions(-) diff --git a/tests/parser/test_errors.py b/tests/parser/test_errors.py index 5c616388..df787adf 100644 --- a/tests/parser/test_errors.py +++ b/tests/parser/test_errors.py @@ -34,44 +34,6 @@ def test_multiple_features_error(pytester): result.stdout.fnmatch_lines(["*FeatureError: Multiple features are not allowed in a single feature file.*"]) -def test_step_outside_scenario_or_background_error(pytester): - """Test step outside of a Scenario or Background.""" - features = pytester.mkdir("features") - features.joinpath("test.feature").write_text( - textwrap.dedent( - """ - Feature: Invalid Feature - # Step not inside a scenario or background - Given a step that is not inside a scenario or background - - Scenario: A valid scenario - Given a step inside a scenario - - """ - ), - encoding="utf-8", - ) - - pytester.makepyfile( - textwrap.dedent( - """ - from pytest_bdd import scenarios, given - - @given("a step inside a scenario") - def step_inside_scenario(): - pass - - scenarios('features') - """ - ) - ) - - result = pytester.runpytest() - - # Expect the FeatureError for the step outside of scenario or background - result.stdout.fnmatch_lines(["*FeatureError: Step definition outside of a Scenario or a Background.*"]) - - def test_multiple_backgrounds_error(pytester): """Test multiple backgrounds in a single feature.""" features = pytester.mkdir("features")