forked from test_framework/pytest-bdd
Make sure step functions can be reused with converters
This commit is contained in:
parent
55f70cf934
commit
36bb1b9f32
|
@ -132,9 +132,9 @@ def _find_step_fixturedef(
|
||||||
if fixturedefs is not None:
|
if fixturedefs is not None:
|
||||||
return fixturedefs
|
return fixturedefs
|
||||||
|
|
||||||
argumented_step_name = find_argumented_step_fixture_name(name, type_, fixturemanager)
|
step_func_context = find_argumented_step_fixture_name(name, type_, fixturemanager)
|
||||||
if argumented_step_name is not None:
|
if step_func_context is not None:
|
||||||
return fixturemanager.getfixturedefs(argumented_step_name, item.nodeid)
|
return fixturemanager.getfixturedefs(step_func_context.name, item.nodeid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ from __future__ import annotations
|
||||||
import collections
|
import collections
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Callable, cast
|
from typing import TYPE_CHECKING, Callable, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -22,6 +23,7 @@ from _pytest.fixtures import FixtureLookupError, FixtureManager, FixtureRequest,
|
||||||
|
|
||||||
from . import exceptions
|
from . import exceptions
|
||||||
from .feature import get_feature, get_features
|
from .feature import get_feature, get_features
|
||||||
|
from .parsers import StepParser
|
||||||
from .steps import get_step_fixture_name, inject_fixture
|
from .steps import get_step_fixture_name, inject_fixture
|
||||||
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
|
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
|
||||||
|
|
||||||
|
@ -36,7 +38,16 @@ PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
||||||
ALPHA_REGEX = re.compile(r"^\d+_*")
|
ALPHA_REGEX = re.compile(r"^\d+_*")
|
||||||
|
|
||||||
|
|
||||||
def find_argumented_step_fixture_name(name: str, type_: str, fixturemanager: FixtureManager) -> str | None:
|
@dataclass
|
||||||
|
class StepFunctionContext:
|
||||||
|
name: str
|
||||||
|
parser: StepParser | None = None
|
||||||
|
converters: dict[str, Callable[..., Any]] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def find_argumented_step_fixture_name(
|
||||||
|
name: str, type_: str, fixturemanager: FixtureManager
|
||||||
|
) -> StepFunctionContext | None:
|
||||||
"""Find argumented step fixture name."""
|
"""Find argumented step fixture name."""
|
||||||
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
|
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
|
||||||
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
|
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
|
||||||
|
@ -50,11 +61,16 @@ def find_argumented_step_fixture_name(name: str, type_: str, fixturemanager: Fix
|
||||||
continue
|
continue
|
||||||
|
|
||||||
parser_name = get_step_fixture_name(parser.name, type_)
|
parser_name = get_step_fixture_name(parser.name, type_)
|
||||||
return parser_name
|
|
||||||
|
return StepFunctionContext(
|
||||||
|
name=parser_name,
|
||||||
|
parser=parser,
|
||||||
|
converters=fixturedef.func._pytest_bdd_converters,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario) -> Callable[..., Any]:
|
def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario) -> StepFunctionContext:
|
||||||
"""Match the step defined by the regular expression pattern.
|
"""Match the step defined by the regular expression pattern.
|
||||||
|
|
||||||
:param request: PyTest request object.
|
:param request: PyTest request object.
|
||||||
|
@ -65,15 +81,18 @@ def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario)
|
||||||
:rtype: function
|
:rtype: function
|
||||||
"""
|
"""
|
||||||
name = step.name
|
name = step.name
|
||||||
try:
|
try: # TODO: wrap this try only to around the part that can raise
|
||||||
# Simple case where no parser is used for the step
|
# Simple case where no parser is used for the step
|
||||||
return request.getfixturevalue(get_step_fixture_name(name, step.type))
|
candidate_name = get_step_fixture_name(name, step.type)
|
||||||
|
request.getfixturevalue(candidate_name)
|
||||||
|
return StepFunctionContext(name=candidate_name)
|
||||||
except FixtureLookupError as e:
|
except FixtureLookupError as e:
|
||||||
try:
|
try:
|
||||||
# Could not find a fixture with the same name, let's see if there is a parser involved
|
# Could not find a fixture with the same name, let's see if there is a parser involved
|
||||||
argumented_name = find_argumented_step_fixture_name(name, step.type, request._fixturemanager)
|
step_func_context = find_argumented_step_fixture_name(name, step.type, request._fixturemanager)
|
||||||
if argumented_name:
|
if step_func_context:
|
||||||
return request.getfixturevalue(argumented_name)
|
request.getfixturevalue(step_func_context.name) # TODO: This shouldn't really be necessary
|
||||||
|
return step_func_context
|
||||||
raise e
|
raise e
|
||||||
except FixtureLookupError as e2:
|
except FixtureLookupError as e2:
|
||||||
raise exceptions.StepDefinitionNotFoundError(
|
raise exceptions.StepDefinitionNotFoundError(
|
||||||
|
@ -83,7 +102,7 @@ def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario)
|
||||||
|
|
||||||
|
|
||||||
def _execute_step_function(
|
def _execute_step_function(
|
||||||
request: FixtureRequest, scenario: Scenario, step: Step, step_func: Callable[..., Any]
|
request: FixtureRequest, scenario: Scenario, step: Step, step_func_context: StepFunctionContext
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Execute step function.
|
"""Execute step function.
|
||||||
|
|
||||||
|
@ -93,30 +112,28 @@ def _execute_step_function(
|
||||||
:param function step_func: Step function.
|
:param function step_func: Step function.
|
||||||
:param example: Example table.
|
:param example: Example table.
|
||||||
"""
|
"""
|
||||||
|
step_func = request.getfixturevalue(step_func_context.name)
|
||||||
kw = {"request": request, "feature": scenario.feature, "scenario": scenario, "step": step, "step_func": step_func}
|
kw = {"request": request, "feature": scenario.feature, "scenario": scenario, "step": step, "step_func": step_func}
|
||||||
|
|
||||||
request.config.hook.pytest_bdd_before_step(**kw)
|
request.config.hook.pytest_bdd_before_step(**kw)
|
||||||
kw["step_func_args"] = {}
|
kw["step_func_args"] = {}
|
||||||
try:
|
try: # TODO: Move this to the places where an exception can actually be raised
|
||||||
# Get the step argument values.
|
# Get the step argument values.
|
||||||
converters = step_func._pytest_bdd_converters
|
converters = step_func_context.converters
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
for parser in step_func._pytest_bdd_parsers:
|
if step_func_context.parser:
|
||||||
if not parser.is_matching(step.name):
|
for arg, value in step_func_context.parser.parse_arguments(step.name).items():
|
||||||
continue
|
|
||||||
for arg, value in parser.parse_arguments(step.name).items():
|
|
||||||
if arg in converters:
|
if arg in converters:
|
||||||
value = converters[arg](value)
|
value = converters[arg](value)
|
||||||
kwargs[arg] = value
|
kwargs[arg] = value
|
||||||
break
|
|
||||||
|
|
||||||
args = get_args(step_func)
|
args = get_args(step_func) # TODO: Add args to step_function_context?
|
||||||
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in args}
|
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in args}
|
||||||
kw["step_func_args"] = kwargs
|
kw["step_func_args"] = kwargs
|
||||||
|
|
||||||
request.config.hook.pytest_bdd_before_step_call(**kw)
|
request.config.hook.pytest_bdd_before_step_call(**kw)
|
||||||
target_fixture = step_func._pytest_bdd_target_fixture
|
target_fixture = step_func._pytest_bdd_target_fixture # TODO: Add target fixture to the step function context
|
||||||
|
|
||||||
# Execute the step as if it was a pytest fixture, so that we can allow "yield" statements in it
|
# Execute the step as if it was a pytest fixture, so that we can allow "yield" statements in it
|
||||||
return_value = call_fixture_func(fixturefunc=step_func, request=request, kwargs=kwargs)
|
return_value = call_fixture_func(fixturefunc=step_func, request=request, kwargs=kwargs)
|
||||||
|
@ -143,13 +160,13 @@ def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequ
|
||||||
# Execute scenario steps
|
# Execute scenario steps
|
||||||
for step in scenario.steps:
|
for step in scenario.steps:
|
||||||
try:
|
try:
|
||||||
step_func = _find_step_function(request, step, scenario)
|
step_func_context = _find_step_function(request, step, scenario)
|
||||||
except exceptions.StepDefinitionNotFoundError as exception:
|
except exceptions.StepDefinitionNotFoundError as exception:
|
||||||
request.config.hook.pytest_bdd_step_func_lookup_error(
|
request.config.hook.pytest_bdd_step_func_lookup_error(
|
||||||
request=request, feature=feature, scenario=scenario, step=step, exception=exception
|
request=request, feature=feature, scenario=scenario, step=step, exception=exception
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
_execute_step_function(request, scenario, step, step_func)
|
_execute_step_function(request, scenario, step, step_func_context)
|
||||||
finally:
|
finally:
|
||||||
request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario)
|
request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario)
|
||||||
|
|
||||||
|
|
|
@ -132,9 +132,10 @@ def _step_decorator(
|
||||||
return func
|
return func
|
||||||
|
|
||||||
lazy_step_func._pytest_bdd_parser = parser_instance
|
lazy_step_func._pytest_bdd_parser = parser_instance
|
||||||
|
lazy_step_func._pytest_bdd_converters = converters
|
||||||
|
|
||||||
setdefault(func, "_pytest_bdd_parsers", []).append(parser_instance)
|
setdefault(func, "_pytest_bdd_parsers", []).append(parser_instance)
|
||||||
func._pytest_bdd_converters = converters
|
|
||||||
func._pytest_bdd_target_fixture = target_fixture
|
func._pytest_bdd_target_fixture = target_fixture
|
||||||
|
|
||||||
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
|
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
from pytest_bdd.utils import collect_dumped_objects
|
||||||
|
|
||||||
|
|
||||||
|
def test_reuse_same_step_different_converters(testdir):
|
||||||
|
testdir.makefile(
|
||||||
|
".feature",
|
||||||
|
arguments=textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
Feature: Reuse same step with different converters
|
||||||
|
Scenario: Step function should be able to be decorated multiple times with different converters
|
||||||
|
Given I have a foo with int value 42
|
||||||
|
And I have a foo with str value 42
|
||||||
|
And I have a foo with float value 42
|
||||||
|
When pass
|
||||||
|
Then pass
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
testdir.makepyfile(
|
||||||
|
textwrap.dedent(
|
||||||
|
r"""
|
||||||
|
import pytest
|
||||||
|
from pytest_bdd import parsers, given, when, then, scenarios
|
||||||
|
from pytest_bdd.utils import dump_obj
|
||||||
|
|
||||||
|
scenarios("arguments.feature")
|
||||||
|
|
||||||
|
@given(parsers.re(r"^I have a foo with int value (?P<value>.*?)$"), converters={"value": int})
|
||||||
|
@given(parsers.re(r"^I have a foo with str value (?P<value>.*?)$"), converters={"value": str})
|
||||||
|
@given(parsers.re(r"^I have a foo with float value (?P<value>.*?)$"), converters={"value": float})
|
||||||
|
def _(value):
|
||||||
|
dump_obj(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@then("pass")
|
||||||
|
@when("pass")
|
||||||
|
def _():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = testdir.runpytest("-s")
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
||||||
|
[int_value, str_value, float_value] = collect_dumped_objects(result)
|
||||||
|
assert type(int_value) is int
|
||||||
|
assert int_value == 42
|
||||||
|
|
||||||
|
assert type(str_value) is str
|
||||||
|
assert str_value == "42"
|
||||||
|
|
||||||
|
assert type(float_value) is float
|
||||||
|
assert float_value == 42.0
|
Loading…
Reference in New Issue