diff --git a/src/pytest_bdd/gherkin_terminal_reporter.py b/src/pytest_bdd/gherkin_terminal_reporter.py index 1e7d6ae7..fa3d466e 100644 --- a/src/pytest_bdd/gherkin_terminal_reporter.py +++ b/src/pytest_bdd/gherkin_terminal_reporter.py @@ -93,7 +93,10 @@ def pytest_runtest_logreport(self, report: TestReport) -> Any: self._tw.write(report.scenario["name"], **scenario_markup) self._tw.write("\n") for step in report.scenario["steps"]: - self._tw.write(f" {step['keyword']} {step['name']}\n", **scenario_markup) + step_markup = ( + {"red": True} if step["failed"] else ({"yellow": True} if step["skipped"] else {"green": True}) + ) + self._tw.write(f" {step['keyword']} {step['name']}\n", **step_markup) self._tw.write(" " + word, **word_markup) self._tw.write("\n\n") else: diff --git a/src/pytest_bdd/hooks.py b/src/pytest_bdd/hooks.py index 9351b2e3..b4475eee 100644 --- a/src/pytest_bdd/hooks.py +++ b/src/pytest_bdd/hooks.py @@ -25,6 +25,10 @@ def pytest_bdd_after_step(request, feature, scenario, step, step_func, step_func """Called after step function is successfully executed.""" +def pytest_bdd_step_skip(request, feature, scenario, step, step_func, step_func_args, exception): + """Called when step function is skipped.""" + + def pytest_bdd_step_error(request, feature, scenario, step, step_func, step_func_args, exception): """Called when step function failed to execute.""" diff --git a/src/pytest_bdd/parser.py b/src/pytest_bdd/parser.py index aa7c4ec7..fe4a8292 100644 --- a/src/pytest_bdd/parser.py +++ b/src/pytest_bdd/parser.py @@ -296,6 +296,7 @@ class Step: indent: int keyword: str failed: bool = field(init=False, default=False) + skipped: bool = field(init=False, default=False) scenario: ScenarioTemplate | None = field(init=False, default=None) background: Background | None = field(init=False, default=None) lines: list[str] = field(init=False, default_factory=list) @@ -308,6 +309,7 @@ def __init__(self, name: str, type: str, indent: int, line_number: int, keyword: self.keyword = keyword self.failed = False + self.skipped = False self.scenario = None self.background = None self.lines = [] diff --git a/src/pytest_bdd/plugin.py b/src/pytest_bdd/plugin.py index 486cdf87..dedf90a3 100644 --- a/src/pytest_bdd/plugin.py +++ b/src/pytest_bdd/plugin.py @@ -87,6 +87,19 @@ def pytest_bdd_before_scenario(request: FixtureRequest, feature: Feature, scenar reporting.before_scenario(request, feature, scenario) +@pytest.hookimpl(tryfirst=True) +def pytest_bdd_step_skip( + request: FixtureRequest, + feature: Feature, + scenario: Scenario, + step: Step, + step_func: Callable, + step_func_args: dict, + exception: Exception, +) -> None: + reporting.step_skip(request, feature, scenario, step, step_func, step_func_args, exception) + + @pytest.hookimpl(tryfirst=True) def pytest_bdd_step_error( request: FixtureRequest, diff --git a/src/pytest_bdd/reporting.py b/src/pytest_bdd/reporting.py index 26e1cb0e..e5a80e5d 100644 --- a/src/pytest_bdd/reporting.py +++ b/src/pytest_bdd/reporting.py @@ -22,6 +22,7 @@ class StepReport: """Step execution report.""" + skipped = False failed = False stopped = None @@ -44,16 +45,19 @@ def serialize(self) -> dict[str, Any]: "type": self.step.type, "keyword": self.step.keyword, "line_number": self.step.line_number, + "skipped": self.skipped, "failed": self.failed, "duration": self.duration, } - def finalize(self, failed: bool) -> None: + def finalize(self, failed: bool, skipped=False) -> None: """Stop collecting information and finalize the report. :param bool failed: Whether the step execution is failed. + :param bool skipped: Indicates if the step execution is skipped. """ self.stopped = time.perf_counter() + self.skipped = skipped self.failed = failed @property @@ -133,6 +137,17 @@ def fail(self) -> None: report.finalize(failed=True) self.add_step_report(report) + def skip(self): + """Stop collecting information and finalize the report as skipped.""" + self.current_step_report.finalize(failed=False, skipped=True) + remaining_steps = self.scenario.steps[len(self.step_reports) :] + + # Skip the rest of the steps and make reports. + for step in remaining_steps: + report = StepReport(step=step) + report.finalize(failed=False, skipped=True) + self.add_step_report(report) + def runtest_makereport(item: Item, call: CallInfo, rep: TestReport) -> None: """Store item in the report object.""" @@ -150,6 +165,19 @@ def before_scenario(request: FixtureRequest, feature: Feature, scenario: Scenari request.node.__scenario_report__ = ScenarioReport(scenario=scenario) +def step_skip( + request: FixtureRequest, + feature: Feature, + scenario: Scenario, + step: Step, + step_func: Callable, + step_func_args: dict, + exception: Exception, +) -> None: + """Finalize the step report as skipped.""" + request.node.__scenario_report__.skip() + + def step_error( request: FixtureRequest, feature: Feature, diff --git a/src/pytest_bdd/scenario.py b/src/pytest_bdd/scenario.py index df7c029c..889826f4 100644 --- a/src/pytest_bdd/scenario.py +++ b/src/pytest_bdd/scenario.py @@ -21,6 +21,7 @@ import pytest from _pytest.fixtures import FixtureDef, FixtureManager, FixtureRequest, call_fixture_func from _pytest.nodes import iterparentnodeids +from _pytest.outcomes import Skipped from . import exceptions from .feature import get_feature, get_features @@ -160,6 +161,9 @@ def _execute_step_function( except Exception as exception: request.config.hook.pytest_bdd_step_error(exception=exception, **kw) raise + except Skipped as exception: + request.config.hook.pytest_bdd_step_skip(exception=exception, **kw) + raise if context.target_fixture is not None: inject_fixture(request, context.target_fixture, return_value) diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index 727a7486..18458f3e 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -25,6 +25,7 @@ def test_step_trace(pytester): feature-tag scenario-passing-tag scenario-failing-tag + scenario-skipping-tag """ ), ) @@ -33,7 +34,7 @@ def test_step_trace(pytester): test=textwrap.dedent( """ @feature-tag - Feature: One passing scenario, one failing scenario + Feature: One passing scenario, one failing scenario, one skipping scenario @scenario-passing-tag Scenario: Passing @@ -45,6 +46,12 @@ def test_step_trace(pytester): Given a passing step And a failing step + @scenario-skipping-tag + Scenario: Skipping + Given a passing step + And a skipping step + And a passing step + Scenario Outline: Outlined Given there are cucumbers When I eat cucumbers @@ -76,6 +83,10 @@ def _(): def _(): raise Exception('Error') + @given('a skipping step') + def _(): + pytest.skip() + @given(parsers.parse('there are {start:d} cucumbers'), target_fixture="cucumbers") def _(start): assert isinstance(start, int) @@ -106,7 +117,7 @@ def _(cucumbers, left): "description": "", "filename": str(feature), "line_number": 2, - "name": "One passing scenario, one failing scenario", + "name": "One passing scenario, one failing scenario, one skipping scenario", "rel_filename": str(relpath), "tags": ["feature-tag"], }, @@ -119,6 +130,7 @@ def _(cucumbers, left): "keyword": "Given", "line_number": 6, "name": "a passing step", + "skipped": False, "type": "given", }, { @@ -127,6 +139,7 @@ def _(cucumbers, left): "keyword": "And", "line_number": 7, "name": "some other passing step", + "skipped": False, "type": "given", }, ], @@ -141,7 +154,7 @@ def _(cucumbers, left): "description": "", "filename": str(feature), "line_number": 2, - "name": "One passing scenario, one failing scenario", + "name": "One passing scenario, one failing scenario, one skipping scenario", "rel_filename": str(relpath), "tags": ["feature-tag"], }, @@ -154,6 +167,7 @@ def _(cucumbers, left): "keyword": "Given", "line_number": 11, "name": "a passing step", + "skipped": False, "type": "given", }, { @@ -162,6 +176,7 @@ def _(cucumbers, left): "keyword": "And", "line_number": 12, "name": "a failing step", + "skipped": False, "type": "given", }, ], @@ -169,41 +184,89 @@ def _(cucumbers, left): } assert report == expected + report = result.matchreport("test_skipping", when="call").scenario + expected = { + "feature": { + "description": "", + "filename": str(feature), + "line_number": 2, + "name": "One passing scenario, one failing scenario, one skipping scenario", + "rel_filename": str(relpath), + "tags": ["feature-tag"], + }, + "line_number": 15, + "name": "Skipping", + "steps": [ + { + "duration": OfType(float), + "failed": False, + "keyword": "Given", + "line_number": 16, + "name": "a passing step", + "skipped": False, + "type": "given", + }, + { + "duration": OfType(float), + "failed": False, + "keyword": "And", + "line_number": 17, + "name": "a skipping step", + "skipped": True, + "type": "given", + }, + { + "duration": OfType(float), + "failed": False, + "keyword": "And", + "line_number": 18, + "name": "a passing step", + "skipped": True, + "type": "given", + }, + ], + "tags": ["scenario-skipping-tag"], + } + assert report == expected + report = result.matchreport("test_outlined[12-5-7]", when="call").scenario expected = { "feature": { "description": "", "filename": str(feature), "line_number": 2, - "name": "One passing scenario, one failing scenario", + "name": "One passing scenario, one failing scenario, one skipping scenario", "rel_filename": str(relpath), "tags": ["feature-tag"], }, - "line_number": 14, + "line_number": 20, "name": "Outlined", "steps": [ { "duration": OfType(float), "failed": False, "keyword": "Given", - "line_number": 15, + "line_number": 21, "name": "there are 12 cucumbers", + "skipped": False, "type": "given", }, { "duration": OfType(float), "failed": False, "keyword": "When", - "line_number": 16, + "line_number": 22, "name": "I eat 5 cucumbers", + "skipped": False, "type": "when", }, { "duration": OfType(float), "failed": False, "keyword": "Then", - "line_number": 17, + "line_number": 23, "name": "I should have 7 cucumbers", + "skipped": False, "type": "then", }, ], @@ -217,35 +280,38 @@ def _(cucumbers, left): "description": "", "filename": str(feature), "line_number": 2, - "name": "One passing scenario, one failing scenario", + "name": "One passing scenario, one failing scenario, one skipping scenario", "rel_filename": str(relpath), "tags": ["feature-tag"], }, - "line_number": 14, + "line_number": 20, "name": "Outlined", "steps": [ { "duration": OfType(float), "failed": False, "keyword": "Given", - "line_number": 15, + "line_number": 21, "name": "there are 5 cucumbers", + "skipped": False, "type": "given", }, { "duration": OfType(float), "failed": False, "keyword": "When", - "line_number": 16, + "line_number": 22, "name": "I eat 4 cucumbers", + "skipped": False, "type": "when", }, { "duration": OfType(float), "failed": False, "keyword": "Then", - "line_number": 17, + "line_number": 23, "name": "I should have 1 cucumbers", + "skipped": False, "type": "then", }, ],