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 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:
|
||||
continue
|
||||
|
||||
if step_func_context.type != step.type:
|
||||
if step_func_context.type is not None and step_func_context.type != step.type:
|
||||
continue
|
||||
|
||||
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}"
|
||||
|
||||
|
||||
# TODO: add stacklevel and test it
|
||||
def scenarios(*feature_paths: str, **kwargs: Any) -> None:
|
||||
"""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:
|
||||
"""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(
|
||||
name: str | StepParser,
|
||||
converters: dict[str, Callable] | None = None,
|
||||
target_fixture: str | None = None,
|
||||
stacklevel: int = 1, # TODO: Add it to the docstring
|
||||
) -> Callable:
|
||||
"""Given step decorator.
|
||||
|
||||
|
@ -87,11 +88,14 @@ def given(
|
|||
|
||||
: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(
|
||||
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:
|
||||
"""When step decorator.
|
||||
|
||||
|
@ -102,11 +106,14 @@ def when(
|
|||
|
||||
: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(
|
||||
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:
|
||||
"""Then step decorator.
|
||||
|
||||
|
@ -117,7 +124,7 @@ def then(
|
|||
|
||||
: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:
|
||||
|
@ -145,6 +152,7 @@ def step(
|
|||
type_: Literal["given", "when", "then"] | None = None,
|
||||
converters: dict[str, Callable] | None = None,
|
||||
target_fixture: str | None = None,
|
||||
stacklevel: int = 1, # TODO: Add it to the docstring
|
||||
) -> Callable[[TCallable], TCallable]:
|
||||
"""Generic step decorator.
|
||||
|
||||
|
@ -179,9 +187,9 @@ def step(
|
|||
|
||||
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(
|
||||
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)
|
||||
return func
|
||||
|
|
|
@ -28,16 +28,18 @@ def get_args(func: Callable) -> list[str]:
|
|||
:rtype: list
|
||||
"""
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
return _getframe(depth).f_locals
|
||||
return _getframe(stacklevel + 1).f_locals
|
||||
|
||||
|
||||
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)
|
||||
assert first_given == ("str",)
|
||||
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