PoC @scenario_class(...) class generator
This commit is contained in:
parent
bf88c44ff4
commit
3700cb4296
|
@ -1,7 +1,7 @@
|
||||||
"""pytest-bdd public API."""
|
"""pytest-bdd public API."""
|
||||||
from __future__ import annotations
|
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
|
from pytest_bdd.steps import given, step, then, when
|
||||||
|
|
||||||
__all__ = ["given", "when", "step", "then", "scenario", "scenarios"]
|
__all__ = ["given", "when", "step", "then", "scenario", "scenarios"]
|
||||||
|
|
|
@ -367,9 +367,54 @@ def scenarios(*feature_paths: str, **kwargs: Any) -> None:
|
||||||
|
|
||||||
for test_name in get_python_name_generator(scenario_name):
|
for test_name in get_python_name_generator(scenario_name):
|
||||||
if test_name not in caller_locals:
|
if test_name not in caller_locals:
|
||||||
# found an unique test name
|
# found a unique test name
|
||||||
caller_locals[test_name] = _scenario
|
caller_locals[test_name] = _scenario
|
||||||
break
|
break
|
||||||
found = True
|
found = True
|
||||||
if not found:
|
if not found:
|
||||||
raise exceptions.NoScenariosFound(abs_feature_paths)
|
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
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
"""Test scenarios shortcut."""
|
"""Test scenarios shortcut."""
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
from pytest_bdd.utils import collect_dumped_objects
|
||||||
|
|
||||||
|
|
||||||
def test_scenarios(testdir, pytest_params):
|
def test_scenarios(testdir, pytest_params):
|
||||||
"""Test scenarios shortcut (used together with @scenario for individual test override)."""
|
"""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"])
|
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):
|
def test_scenarios_none_found(testdir, pytest_params):
|
||||||
"""Test scenarios shortcut when no scenarios found."""
|
"""Test scenarios shortcut when no scenarios found."""
|
||||||
testpath = testdir.makepyfile(
|
testpath = testdir.makepyfile(
|
||||||
|
|
Loading…
Reference in New Issue