forked from test_framework/pytest-bdd
Add stacklevel param to the steps, implement a test that makes use of it
This commit is contained in:
parent
865a897bcf
commit
a16d246842
|
@ -2,6 +2,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pytest_bdd.scenario import scenario, scenarios
|
from pytest_bdd.scenario import scenario, scenarios
|
||||||
from pytest_bdd.steps import given, then, when
|
from pytest_bdd.steps import given, step, then, when
|
||||||
|
|
||||||
__all__ = ["given", "when", "then", "scenario", "scenarios"]
|
__all__ = ["given", "when", "step", "then", "scenario", "scenarios"]
|
||||||
|
|
|
@ -52,7 +52,7 @@ def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, nodeid
|
||||||
if step_func_context is None:
|
if step_func_context is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if step_func_context.type != step.type:
|
if step_func_context.type is not None and step_func_context.type != step.type:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
match = step_func_context.parser.is_matching(step.name)
|
match = step_func_context.parser.is_matching(step.name)
|
||||||
|
@ -331,6 +331,7 @@ def get_python_name_generator(name: str) -> Iterable[str]:
|
||||||
suffix = f"_{index}"
|
suffix = f"_{index}"
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: add stacklevel and test it
|
||||||
def scenarios(*feature_paths: str, **kwargs: Any) -> None:
|
def scenarios(*feature_paths: str, **kwargs: Any) -> None:
|
||||||
"""Parse features from the paths and put all found scenarios in the caller module.
|
"""Parse features from the paths and put all found scenarios in the caller module.
|
||||||
|
|
||||||
|
|
|
@ -70,13 +70,14 @@ class StepFunctionContext:
|
||||||
|
|
||||||
def get_step_fixture_name(step: Step) -> str:
|
def get_step_fixture_name(step: Step) -> str:
|
||||||
"""Get step fixture name"""
|
"""Get step fixture name"""
|
||||||
return f"{StepNamePrefix.step_impl}_{step.type}_{step.name}"
|
return f"{StepNamePrefix.step_impl.value}_{step.type}_{step.name}"
|
||||||
|
|
||||||
|
|
||||||
def given(
|
def given(
|
||||||
name: str | StepParser,
|
name: str | StepParser,
|
||||||
converters: dict[str, Callable] | None = None,
|
converters: dict[str, Callable] | None = None,
|
||||||
target_fixture: str | None = None,
|
target_fixture: str | None = None,
|
||||||
|
stacklevel: int = 1, # TODO: Add it to the docstring
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
"""Given step decorator.
|
"""Given step decorator.
|
||||||
|
|
||||||
|
@ -87,11 +88,14 @@ def given(
|
||||||
|
|
||||||
:return: Decorator function for the step.
|
:return: Decorator function for the step.
|
||||||
"""
|
"""
|
||||||
return step(name, GIVEN, converters=converters, target_fixture=target_fixture)
|
return step(name, GIVEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
|
||||||
|
|
||||||
|
|
||||||
def when(
|
def when(
|
||||||
name: str | StepParser, converters: dict[str, Callable] | None = None, target_fixture: str | None = None
|
name: str | StepParser,
|
||||||
|
converters: dict[str, Callable] | None = None,
|
||||||
|
target_fixture: str | None = None,
|
||||||
|
stacklevel: int = 1, # TODO: Add it to the docstring
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
"""When step decorator.
|
"""When step decorator.
|
||||||
|
|
||||||
|
@ -102,11 +106,14 @@ def when(
|
||||||
|
|
||||||
:return: Decorator function for the step.
|
:return: Decorator function for the step.
|
||||||
"""
|
"""
|
||||||
return step(name, WHEN, converters=converters, target_fixture=target_fixture)
|
return step(name, WHEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
|
||||||
|
|
||||||
|
|
||||||
def then(
|
def then(
|
||||||
name: str | StepParser, converters: dict[str, Callable] | None = None, target_fixture: str | None = None
|
name: str | StepParser,
|
||||||
|
converters: dict[str, Callable] | None = None,
|
||||||
|
target_fixture: str | None = None,
|
||||||
|
stacklevel: int = 1, # TODO: Add it to the docstring
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
"""Then step decorator.
|
"""Then step decorator.
|
||||||
|
|
||||||
|
@ -117,7 +124,7 @@ def then(
|
||||||
|
|
||||||
:return: Decorator function for the step.
|
:return: Decorator function for the step.
|
||||||
"""
|
"""
|
||||||
return step(name, THEN, converters=converters, target_fixture=target_fixture)
|
return step(name, THEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
|
||||||
|
|
||||||
|
|
||||||
def find_unique_name(name: str, seen: Iterable[str]) -> str:
|
def find_unique_name(name: str, seen: Iterable[str]) -> str:
|
||||||
|
@ -145,6 +152,7 @@ def step(
|
||||||
type_: Literal["given", "when", "then"] | None = None,
|
type_: Literal["given", "when", "then"] | None = None,
|
||||||
converters: dict[str, Callable] | None = None,
|
converters: dict[str, Callable] | None = None,
|
||||||
target_fixture: str | None = None,
|
target_fixture: str | None = None,
|
||||||
|
stacklevel: int = 1, # TODO: Add it to the docstring
|
||||||
) -> Callable[[TCallable], TCallable]:
|
) -> Callable[[TCallable], TCallable]:
|
||||||
"""Generic step decorator.
|
"""Generic step decorator.
|
||||||
|
|
||||||
|
@ -179,9 +187,9 @@ def step(
|
||||||
|
|
||||||
step_function_marker._pytest_bdd_step_context = context
|
step_function_marker._pytest_bdd_step_context = context
|
||||||
|
|
||||||
caller_locals = get_caller_module_locals()
|
caller_locals = get_caller_module_locals(stacklevel=stacklevel)
|
||||||
fixture_step_name = find_unique_name(
|
fixture_step_name = find_unique_name(
|
||||||
f"{StepNamePrefix.step_def}_{type_ or '*'}_{parser.name}", seen=caller_locals.keys()
|
f"{StepNamePrefix.step_def.value}_{type_ or '*'}_{parser.name}", seen=caller_locals.keys()
|
||||||
)
|
)
|
||||||
caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(step_function_marker)
|
caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(step_function_marker)
|
||||||
return func
|
return func
|
||||||
|
|
|
@ -28,16 +28,18 @@ def get_args(func: Callable) -> list[str]:
|
||||||
:rtype: list
|
:rtype: list
|
||||||
"""
|
"""
|
||||||
params = signature(func).parameters.values()
|
params = signature(func).parameters.values()
|
||||||
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
|
return [
|
||||||
|
param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD and param.default is param.empty
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_caller_module_locals(depth: int = 2) -> dict[str, Any]:
|
def get_caller_module_locals(stacklevel: int = 1) -> dict[str, Any]:
|
||||||
"""Get the caller module locals dictionary.
|
"""Get the caller module locals dictionary.
|
||||||
|
|
||||||
We use sys._getframe instead of inspect.stack(0) because the latter is way slower, since it iterates over
|
We use sys._getframe instead of inspect.stack(0) because the latter is way slower, since it iterates over
|
||||||
all the frames in the stack.
|
all the frames in the stack.
|
||||||
"""
|
"""
|
||||||
return _getframe(depth).f_locals
|
return _getframe(stacklevel + 1).f_locals
|
||||||
|
|
||||||
|
|
||||||
def get_caller_module_path(depth: int = 2) -> str:
|
def get_caller_module_path(depth: int = 2) -> str:
|
||||||
|
|
|
@ -97,3 +97,150 @@ def test_step_functions_same_parser(testdir):
|
||||||
[first_given, second_given] = collect_dumped_objects(result)
|
[first_given, second_given] = collect_dumped_objects(result)
|
||||||
assert first_given == ("str",)
|
assert first_given == ("str",)
|
||||||
assert second_given == ("re", "testfoo")
|
assert second_given == ("re", "testfoo")
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_implements_a_step_generator(testdir):
|
||||||
|
testdir.makefile(
|
||||||
|
".feature",
|
||||||
|
user_step_generator=textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
Feature: A feature
|
||||||
|
Scenario: A scenario
|
||||||
|
Given I have 10 EUR
|
||||||
|
And the wallet is verified
|
||||||
|
And I have a wallet
|
||||||
|
When I pay 1 EUR
|
||||||
|
Then I should have 9 EUR in my wallet
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
testdir.makepyfile(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, fields
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pytest_bdd import given, when, then, scenarios, parsers
|
||||||
|
from pytest_bdd.utils import dump_obj
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Wallet:
|
||||||
|
verified: bool
|
||||||
|
|
||||||
|
amount_eur: int
|
||||||
|
amount_usd: int
|
||||||
|
amount_gbp: int
|
||||||
|
amount_jpy: int
|
||||||
|
|
||||||
|
def pay(self, amount: int, currency: str) -> None:
|
||||||
|
if not self.verified:
|
||||||
|
raise ValueError("Wallet account is not verified")
|
||||||
|
currency = currency.lower()
|
||||||
|
field = f"amount_{currency}"
|
||||||
|
setattr(self, field, getattr(self, field) - amount)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def wallet__verified():
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def wallet__amount_eur():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def wallet__amount_usd():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def wallet__amount_gbp():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def wallet__amount_jpy():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def wallet(
|
||||||
|
wallet__verified,
|
||||||
|
wallet__amount_eur,
|
||||||
|
wallet__amount_usd,
|
||||||
|
wallet__amount_gbp,
|
||||||
|
wallet__amount_jpy,
|
||||||
|
):
|
||||||
|
return Wallet(
|
||||||
|
verified=wallet__verified,
|
||||||
|
amount_eur=wallet__amount_eur,
|
||||||
|
amount_usd=wallet__amount_usd,
|
||||||
|
amount_gbp=wallet__amount_gbp,
|
||||||
|
amount_jpy=wallet__amount_jpy,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wallet_steps_generator(model_name="wallet"):
|
||||||
|
@given("I have a wallet", target_fixture=model_name, stacklevel=2)
|
||||||
|
def _(wallet):
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
@given(
|
||||||
|
parsers.re(r"the wallet is (?P<negation>not)?verified"),
|
||||||
|
target_fixture=f"{model_name}__verified",
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
def _(negation: str):
|
||||||
|
if negation:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Generate steps for currency fields:
|
||||||
|
for field in fields(Wallet):
|
||||||
|
match = re.fullmatch(r"amount_(?P<currency>[a-z]{3})", field.name)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
currency = match["currency"]
|
||||||
|
|
||||||
|
@given(
|
||||||
|
parsers.parse(f"I have {{value:d}} {currency.upper()}"),
|
||||||
|
target_fixture=f"{model_name}__amount_{currency}",
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
def _(value: int, _currency=currency) -> int:
|
||||||
|
dump_obj(f"given {value} {_currency.upper()}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
@when(
|
||||||
|
parsers.parse(f"I pay {{value:d}} {currency.upper()}"),
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
def _(wallet: Wallet, value: int, _currency=currency) -> None:
|
||||||
|
dump_obj(f"pay {value} {_currency.upper()}")
|
||||||
|
wallet.pay(value, _currency)
|
||||||
|
|
||||||
|
@then(
|
||||||
|
parsers.parse(f"I should have {{value:d}} {currency.upper()} in my wallet"),
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
def _(wallet: Wallet, value: int, _currency=currency) -> None:
|
||||||
|
dump_obj(f"assert {value} {_currency.upper()}")
|
||||||
|
assert getattr(wallet, f"amount_{_currency}") == value
|
||||||
|
|
||||||
|
wallet_steps_generator()
|
||||||
|
|
||||||
|
scenarios("user_step_generator.feature")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = testdir.runpytest("-s")
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
||||||
|
[given, pay, assert_] = collect_dumped_objects(result)
|
||||||
|
assert given == "given 10 EUR"
|
||||||
|
assert pay == "pay 1 EUR"
|
||||||
|
assert assert_ == "assert 9 EUR"
|
||||||
|
|
Loading…
Reference in New Issue