From ba33f181336ba61c2038445776205cd5efe6d506 Mon Sep 17 00:00:00 2001 From: Jason Allen Date: Mon, 11 Nov 2024 20:16:48 +0000 Subject: [PATCH] Implement multiple example tables --- CHANGES.rst | 2 ++ README.rst | 65 +++++++++++++++++++++++++++++++++++ src/pytest_bdd/scenario.py | 12 ++++--- tests/feature/test_outline.py | 19 ++++------ tests/feature/test_tags.py | 44 ++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 17 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 400f2c5..661df2d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,8 @@ Unreleased - Text after the `#` character is no longer stripped from the Scenario and Feature name. - Gherkin keyword aliases can now be used and correctly reported in json and terminal output (see `Keywords ` for permitted list). - Added localization support. The language of the feature file can be specified using the `# language: ` directive at the beginning of the file. +- Multiple example tables supported +- Added filtering by tags against example tables 8.0.0b2 ---------- diff --git a/README.rst b/README.rst index 9b9bf88..4559844 100644 --- a/README.rst +++ b/README.rst @@ -514,6 +514,71 @@ Example: assert cucumbers["start"] - cucumbers["eat"] == left +Scenario Outlines with Multiple Example Tables +---------------------------------------------- + +In `pytest-bdd`, you can use multiple example tables in a scenario outline to test +different sets of input data under various conditions. +You can define separate `Examples` blocks, each with its own table of data, +and optionally tag them to differentiate between positive, negative, or any other conditions. + +Example: + +.. code-block:: gherkin + + # content of scenario_outline.feature + + Feature: Scenario outlines with multiple examples tables + Scenario Outline: Outlined with multiple example tables + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + @positive + Examples: Positive results + | start | eat | left | + | 12 | 5 | 7 | + | 5 | 4 | 1 | + + @negative + Examples: Impossible negative results + | start | eat | left | + | 3 | 9 | -6 | + | 1 | 4 | -3 | + +.. code-block:: python + + from pytest_bdd import scenarios, given, when, then, parsers + + + scenarios("scenario_outline.feature") + + + @given(parsers.parse("there are {start:d} cucumbers"), target_fixture="cucumbers") + def given_cucumbers(start): + return {"start": start, "eat": 0} + + + @when(parsers.parse("I eat {eat:d} cucumbers")) + def eat_cucumbers(cucumbers, eat): + cucumbers["eat"] += eat + + + @then(parsers.parse("I should have {left:d} cucumbers")) + def should_have_left_cucumbers(cucumbers, left): + assert cucumbers["start"] - cucumbers["eat"] == left + + +When you filter scenarios by a tag, only the examples associated with that tag will be executed. +This allows you to run a specific subset of your test cases based on the tag. +For example, in the following scenario outline, if you filter by the @positive tag, +only the examples under the "Positive results" table will be executed, and the "Negative results" table will be ignored. + +.. code-block:: bash + + pytest -k "positive" + + Datatables ---------- diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index c80d5ea..6ffcd0f 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -32,7 +32,6 @@ from .steps import StepFunctionContext, get_step_fixture_name from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path if TYPE_CHECKING: - from _pytest.mark.structures import ParameterSet from _pytest.nodes import Node from .parser import Feature, Scenario, ScenarioTemplate, Step @@ -303,13 +302,17 @@ def _get_scenario_decorator( def collect_example_parametrizations( templated_scenario: ScenarioTemplate, -) -> list[ParameterSet] | None: +) -> list[pytest.ParameterSet] | None: parametrizations = [] has_multiple_examples = len(templated_scenario.examples) > 1 for example_id, examples in enumerate(templated_scenario.examples): - with warns(PytestUnknownMarkWarning, match=r"Unknown pytest\.mark\.tag"): - example_marks = [pytest.mark.tag(tag) for tag in examples.tags] + _tags = examples.tags or [] + example_marks = [] + if _tags: + with warns(PytestUnknownMarkWarning, match=r"Unknown pytest\.mark\.\w+"): + example_marks = [pytest.mark.__getattr__(tag) for tag in _tags] + for context in examples.as_contexts() or [{}]: test_id = "-".join((str(example_id), *context.values())) if has_multiple_examples else "-".join( context.values()) @@ -323,7 +326,6 @@ def collect_example_parametrizations( return parametrizations or None - def scenario( feature_name: str, scenario_name: str, diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index b71cda7..65934a6 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -82,22 +82,22 @@ def test_outlined(pytester): def test_multiple_outlined(pytester): pytester.makefile( ".feature", - outline=textwrap.dedent( + outline_multi_example=textwrap.dedent( """\ - Feature: Outline + Feature: Outline With Multiple Examples Scenario Outline: Outlined given, when, thens with multiple examples tables Given there are cucumbers When I eat cucumbers Then I should have cucumbers @positive - Examples: Positive result + Examples: Positive results | start | eat | left | | 12 | 5 | 7 | | 5 | 4 | 1 | @negative - Examples: Negative result + Examples: Negative results | start | eat | left | | 3 | 9 | -6 | | 1 | 4 | -3 | @@ -110,15 +110,10 @@ def test_multiple_outlined(pytester): pytester.makepyfile( textwrap.dedent( """\ - from pytest_bdd import scenario - - @scenario( - "outline.feature", - "Outlined given, when, thens with multiple examples tables", - ) - def test_outline(request): - pass + from pytest_bdd import scenarios + scenarios('outline_multi_example.feature') + """ ) ) diff --git a/tests/feature/test_tags.py b/tests/feature/test_tags.py index 781f36d..44ee065 100644 --- a/tests/feature/test_tags.py +++ b/tests/feature/test_tags.py @@ -226,3 +226,47 @@ def test_multiline_tags(pytester): result = pytester.runpytest("-m", "tag2", "-vv") result.assert_outcomes(passed=1, deselected=1) + + +def test_tags_against_multiple_examples_tables(pytester): + pytester.makefile( + ".feature", + test="""\ + Feature: Scenario with tags over multiple lines + + Scenario Outline: Tags + Given I have a + + @food + Examples: Food + | item | + | bun | + | ice | + + @drink + Examples: Drinks + | item | + | water | + | juice | + """, + ) + pytester.makepyfile( + """ + from pytest_bdd import given, scenarios, parsers + + scenarios('test.feature') + + @given(parsers.parse('I have a {item}')) + def _(item: str): + pass + """ + ) + + result = pytester.runpytest("-m", "food", "-vv") + result.assert_outcomes(passed=2, deselected=2) + + result = pytester.runpytest("-m", "drink", "-vv") + result.assert_outcomes(passed=2, deselected=2) + + result = pytester.runpytest("-m", "food or drink", "-vv") + result.assert_outcomes(passed=4, deselected=0)