Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multiple values in target_fixture #611

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/pytest_bdd/scenario.py
Expand Up @@ -162,7 +162,22 @@ def _execute_step_function(
raise

if context.target_fixture is not None:
inject_fixture(request, context.target_fixture, return_value)
if isinstance(return_value, Exception):
arg = context.target_exception if context.target_exception else "response"
inject_fixture(request=request, arg=arg, value=return_value)
else:
target_fixture_tokens = [token for token in context.target_fixture.split(",") if token]
# Single return value in target_fixture
if len(target_fixture_tokens) == 1:
inject_fixture(request=request, arg=target_fixture_tokens[0], value=return_value)
# Multiple comma separated return values in target_fixture
else:
return_values = (return_value,) if not isinstance(return_value, tuple) else return_value
assert len(target_fixture_tokens) == len(
return_values
), f"Return value count: {len(return_values)} are not matching target_fixture count: {len(target_fixture_tokens)}"
for token, value in zip(target_fixture_tokens, return_values):
inject_fixture(request=request, arg=token, value=value)

request.config.hook.pytest_bdd_after_step(**kw)

Expand Down
15 changes: 14 additions & 1 deletion src/pytest_bdd/steps.py
Expand Up @@ -66,6 +66,7 @@ class StepFunctionContext:
parser: StepParser
converters: dict[str, Callable[..., Any]] = field(default_factory=dict)
target_fixture: str | None = None
target_exception: str | None = None


def get_step_fixture_name(step: Step) -> str:
Expand Down Expand Up @@ -96,6 +97,7 @@ def when(
name: str | StepParser,
converters: dict[str, Callable] | None = None,
target_fixture: str | None = None,
target_exception: str | None = None,
stacklevel: int = 1,
) -> Callable:
"""When step decorator.
Expand All @@ -104,11 +106,19 @@ def when(
:param converters: Optional `dict` of the argument or parameter converters in form
{<param_name>: <converter function>}.
:param target_fixture: Target fixture name to replace by steps definition function.
:param target_exception: Target exception name to receive Exception object
:param stacklevel: Stack level to find the caller frame. This is used when injecting the step definition fixture.

:return: Decorator function for the step.
"""
return step(name, WHEN, converters=converters, target_fixture=target_fixture, stacklevel=stacklevel)
return step(
name,
WHEN,
converters=converters,
target_fixture=target_fixture,
target_exception=target_exception,
stacklevel=stacklevel,
)


def then(
Expand All @@ -135,6 +145,7 @@ def step(
type_: Literal["given", "when", "then"] | None = None,
converters: dict[str, Callable] | None = None,
target_fixture: str | None = None,
target_exception: str | None = None,
stacklevel: int = 1,
) -> Callable[[TCallable], TCallable]:
"""Generic step decorator.
Expand All @@ -143,6 +154,7 @@ def step(
:param type_: Step type ("given", "when" or "then"). If None, this step will work for all the types.
:param converters: Optional step arguments converters mapping.
:param target_fixture: Optional fixture name to replace by step definition.
:param target_exception: Optional target exception name
:param stacklevel: Stack level to find the caller frame. This is used when injecting the step definition fixture.

:return: Decorator function for the step.
Expand All @@ -165,6 +177,7 @@ def decorator(func: TCallable) -> TCallable:
parser=parser,
converters=converters,
target_fixture=target_fixture,
target_exception=target_exception,
)

def step_function_marker() -> StepFunctionContext:
Expand Down
46 changes: 46 additions & 0 deletions tests/feature/test_steps.py
Expand Up @@ -584,3 +584,49 @@ def _(stuff):
"*Tearing down...*",
]
)


def test_when_exception(pytester):
pytester.makefile(
".feature",
when_exception=textwrap.dedent(
"""\
Feature: When exception
Scenario: Test when exception is generated
When I have injected exception
Then Exception object should be received as default 'response' param
When I have injected exception with target_exception='exception_ret'
Then Exception object should be received as target_exception='exception_ret'
"""
),
)
pytester.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import when, then, scenario

@scenario("when_exception.feature", "Test when exception is generated")
def test_when_exception():
pass

@when("I have injected exception", target_fixture="foo")
def _():
return Exception("Dummy Exception obj")

@when("I have injected exception with target_exception='exception_ret'", target_fixture="foo", target_exception="exception_ret")
def _():
return Exception("Dummy Exception obj")

@then("Exception object should be received as default 'response' param")
def _(response):
assert isinstance(response, Exception), "response is not Exception object"

@then("Exception object should be received as target_exception='exception_ret'")
def _(exception_ret):
assert isinstance(exception_ret, Exception), "response is not Exception object"
"""
)
)
result = pytester.runpytest()
result.assert_outcomes(passed=1)
54 changes: 51 additions & 3 deletions tests/steps/test_given.py
Expand Up @@ -2,15 +2,17 @@
import textwrap


def test_given_injection(pytester):
def test_given_injection_single_value(pytester):
pytester.makefile(
".feature",
given=textwrap.dedent(
"""\
Feature: Given
Scenario: Test given fixture injection
Given I have injecting given
Given I have injected single value
Then foo should be "injected foo"
Given I have injected tuple value
Then foo should be tuple value
"""
),
)
Expand All @@ -24,15 +26,61 @@ def test_given_injection(pytester):
def test_given():
pass

@given("I have injecting given", target_fixture="foo")
@given("I have injected single value", target_fixture="foo")
def _():
return "injected foo"

@given("I have injected tuple value", target_fixture="foo")
def _():
return ("injected foo", {"city": ["Boston", "Houston"]}, [10,20,30],)

@then('foo should be "injected foo"')
def _(foo):
assert foo == "injected foo"

@then('foo should be tuple value')
def _(foo):
assert foo == ("injected foo", {"city": ["Boston", "Houston"]}, [10,20,30],)
"""
)
)
result = pytester.runpytest()
result.assert_outcomes(passed=1)


def test_given_injection_multiple_values(pytester):
pytester.makefile(
".feature",
given=textwrap.dedent(
"""\
Feature: Given
Scenario: Test given fixture injection
Given I have injecting given values
Then values should be received
"""
),
)
pytester.makepyfile(
textwrap.dedent(
"""\
import pytest
from pytest_bdd import given, then, scenario

@scenario("given.feature", "Test given fixture injection")
def test_given():
pass

@given("I have injecting given values", target_fixture="foo,city,numbers")
def _():
return ("injected foo", {"city": ["Boston", "Houston"]}, [10,20,30],)


@then("values should be received")
def _(foo, city, numbers):
assert foo == "injected foo"
assert city == {"city": ["Boston", "Houston"]}
assert numbers == [10,20,30]

"""
)
)
Expand Down