PoC @scenario_class(...) class generator

This commit is contained in:
Alessio Bogon 2022-07-31 07:05:36 +02:00
parent bf88c44ff4
commit 3700cb4296
3 changed files with 143 additions and 2 deletions

View File

@ -1,7 +1,7 @@
"""pytest-bdd public API."""
from __future__ import annotations
from pytest_bdd.scenario import scenario, scenarios
from pytest_bdd.scenario import scenario, scenario_class, scenarios
from pytest_bdd.steps import given, step, then, when
__all__ = ["given", "when", "step", "then", "scenario", "scenarios"]

View File

@ -367,9 +367,54 @@ def scenarios(*feature_paths: str, **kwargs: Any) -> None:
for test_name in get_python_name_generator(scenario_name):
if test_name not in caller_locals:
# found an unique test name
# found a unique test name
caller_locals[test_name] = _scenario
break
found = True
if not found:
raise exceptions.NoScenariosFound(abs_feature_paths)
# TODO: Test this
def to_camel_case(string: str) -> str:
"""Convert a string to camelCase."""
words = [word.strip() for word in re.split(r"([A-Z][a-z]+)", string)]
return "".join(word.capitalize() for word in words if word.isalpha())
def scenario_class(feature_path: str, cls_name: str | None = None, **kwargs: Any) -> type[object]:
caller_path = get_caller_module_path()
features_base_dir = kwargs.get("features_base_dir")
if features_base_dir is None:
features_base_dir = get_features_base_dir(caller_path)
if not os.path.isabs(feature_path):
feature_path = os.path.abspath(os.path.join(features_base_dir, feature_path))
base, name = os.path.split(feature_path)
feature = get_feature(base, name, **kwargs)
if cls_name is None:
cls_name = to_camel_case(feature.name)
cls = type(cls_name, (), {})
seen_test_names = set()
for scenario_name, _ in feature.scenarios.items():
@staticmethod # TODO: We should not require this.
@scenario(feature.filename, scenario_name, **kwargs)
def _test_scenario() -> None:
pass # pragma: no cover
for test_name in get_python_name_generator(scenario_name):
# add it to the test class
if test_name in seen_test_names:
continue
_test_scenario.__name__ = test_name
setattr(cls, test_name, _test_scenario)
break
return cls

View File

@ -1,6 +1,8 @@
"""Test scenarios shortcut."""
import textwrap
from pytest_bdd.utils import collect_dumped_objects
def test_scenarios(testdir, pytest_params):
"""Test scenarios shortcut (used together with @scenario for individual test override)."""
@ -73,6 +75,100 @@ def test_scenarios(testdir, pytest_params):
result.stdout.fnmatch_lines(["*test_test_scenario_1 *bar!", "PASSED"])
def test_scenarios_class(testdir):
testdir.makepyfile(
"""
import pytest
from pytest_bdd import given, scenario_class
from pytest_bdd.utils import dump_obj
@given('I have a bar')
def _():
dump_obj('bar')
return 'bar'
@given('I have a foo')
def _():
dump_obj('foo')
return 'foo'
TestMyFeature = scenario_class("my_feature.feature", name="TestMyFeature")
"""
)
testdir.makefile(
"feature",
my_feature="""\
Feature: Test feature
Scenario: Scenario foo
Given I have a foo
Scenario: Scenario bar
Given I have a bar
""",
)
result = testdir.runpytest("-s", "-v", "-o", "console_output_style=classic")
result.assert_outcomes(passed=2)
result.stdout.fnmatch_lines(["*TestMyFeature::test_scenario_bar*", "PASSED"])
result.stdout.fnmatch_lines(["*TestMyFeature::test_scenario_foo*", "PASSED"])
objs = collect_dumped_objects(result)
assert objs == ["bar", "foo"]
def test_scenarios_class_can_override_generated_tests(testdir):
testdir.makepyfile(
"""
import pytest
from pytest_bdd import given, scenario_class, parsers, scenario
from pytest_bdd.utils import dump_obj
@given(parsers.parse('I have a {value}'))
def _(value: str) -> str:
dump_obj(f"Given I have a {value}")
return value
class TestMyFeature(scenario_class("my_feature.feature")):
# TODO: We should not be required to use @staticmethod here
@staticmethod
# TODO: We should not be required to pass "my_feature.feature" here
@scenario("my_feature.feature", "Scenario foo")
def test_my_scenario_foo():
dump_obj("overriding scenario foo")
@pytest.mark.skip(reason="testing marker")
# TODO: We should not be required to use @staticmethod here
@staticmethod
@scenario("my_feature.feature", "Scenario bar")
def test_my_scenario_bar_skipped():
dump_obj("this should not be executed")
"""
)
testdir.makefile(
"feature",
my_feature="""\
Feature: Test feature
Scenario: Scenario bar
Given I have a bar
Scenario: Scenario foo
Given I have a foo
Scenario: Scenario baz
Given I have a baz
""",
)
result = testdir.runpytest("-s", "-v", "-o", "console_output_style=classic")
result.assert_outcomes(passed=2, skipped=1)
result.stdout.fnmatch_lines(["*TestMyFeature::test_my_scenario_foo*", "PASSED"])
result.stdout.fnmatch_lines(["*TestMyFeature::test_my_scenario_bar_skipped*", "SKIPPED"])
result.stdout.fnmatch_lines(["*TestMyFeature::test_scenario_baz*", "PASSED"])
[skip_msgs] = collect_dumped_objects(result)
assert skip_msgs == ["overriding scenario foo", "Given I have a baz"]
def test_scenarios_none_found(testdir, pytest_params):
"""Test scenarios shortcut when no scenarios found."""
testpath = testdir.makepyfile(