diff --git a/CHANGES.rst b/CHANGES.rst index ae0b8c5..c40162e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Changelog - Fix FixtureDef signature for newer pytest versions (The-Compiler) - Better error explanation for the steps defined outside of scenarios (olegpidsadnyi) +- Add a ``pytest_bdd_apply_tag`` hook to customize handling of tags (The-Compiler) 2.16.1 diff --git a/README.rst b/README.rst index 0017117..2813dc3 100644 --- a/README.rst +++ b/README.rst @@ -747,6 +747,19 @@ Note that if you use pytest `--strict` option, all bdd tags mentioned in the fea `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. +You can customize how hooks are converted to pytest marks by implementing the +``pytest_bdd_apply_tag`` hook and returning ``True`` from it: + +.. code-block:: python + + def pytest_bdd_apply_tag(tag, function): + if tag == 'todo': + marker = pytest.mark.skip(reason="Not implemented yet") + marker(function) + return True + else: + # Fall back to pytest-bdd's default behavior + return None Test setup ---------- diff --git a/pytest_bdd/hooks.py b/pytest_bdd/hooks.py index 83d338a..b3ec01e 100644 --- a/pytest_bdd/hooks.py +++ b/pytest_bdd/hooks.py @@ -1,3 +1,5 @@ +import pytest + """Pytest-bdd pytest hooks.""" @@ -31,3 +33,13 @@ def pytest_bdd_step_validation_error(request, feature, scenario, step, step_func def pytest_bdd_step_func_lookup_error(request, feature, scenario, step, exception): """Called when step lookup failed.""" + + +@pytest.hookspec(firstresult=True) +def pytest_bdd_apply_tag(tag, function): + """Apply a tag (from a ``.feature`` file) to the given scenario. + + The default implementation does the equivalent of + ``getattr(pytest.mark, tag)(function)``, but you can override this hook and + return ``True`` to do more sophisticated handling of tags. + """ diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index 39e0b0d..6a4b3ca 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -74,3 +74,9 @@ def pytest_bdd_after_step(request, feature, scenario, step, step_func, step_func def pytest_cmdline_main(config): generation.cmdline_main(config) + + +def pytest_bdd_apply_tag(tag, function): + mark = getattr(pytest.mark, tag) + mark(function) + return True diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 0a57830..8255e53 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -269,7 +269,7 @@ def _get_scenario_decorator(feature, feature_name, scenario, scenario_name, call _scenario = pytest.mark.parametrize(*param_set)(_scenario) for tag in scenario.tags.union(feature.tags): - _scenario = getattr(pytest.mark, tag)(_scenario) + pytest.config.hook.pytest_bdd_apply_tag(tag=tag, function=_scenario) _scenario.__doc__ = "{feature_name}: {scenario_name}".format( feature_name=feature_name, scenario_name=scenario_name) diff --git a/tests/feature/test_tags.py b/tests/feature/test_tags.py index ce23fdb..500afa1 100644 --- a/tests/feature/test_tags.py +++ b/tests/feature/test_tags.py @@ -95,3 +95,47 @@ def test_tags_after_background_issue_160(testdir): result = testdir.runpytest('-m', 'tag', '-vv').parseoutcomes() assert result['passed'] == 1 assert result['deselected'] == 1 + + +def test_apply_tag_hook(testdir): + testdir.makeconftest(""" + import pytest + + @pytest.hookimpl(tryfirst=True) + def pytest_bdd_apply_tag(tag, function): + if tag == 'todo': + marker = pytest.mark.skipif(True, reason="Not implemented yet") + marker(function) + return True + else: + # Fall back to pytest-bdd's default behavior + return None + """) + testdir.makefile('.feature', test=""" + Feature: Customizing tag handling + + @todo + Scenario: Tags + Given I have a bar + + @skip + Scenario: Tags 2 + Given I have a bar + """) + testdir.makepyfile(""" + from pytest_bdd import given, scenarios + + @given('I have a bar') + def i_have_bar(): + return 'bar' + + scenarios('test.feature') + """) + result = testdir.runpytest('-rs') + result.stdout.fnmatch_lines( + [ + "SKIP *: Not implemented yet", + "SKIP *: unconditional skip", + "*= 2 skipped * =*" + ] + )