Merge pull request #534 from pytest-dev/reusable-step-functions
Reusable step functions
This commit is contained in:
commit
e24aee0028
|
@ -1,6 +1,11 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
Unreleased
|
||||
----------
|
||||
- Fix bug where steps without parsers would take precedence over steps with parsers. `#534 <https://github.com/pytest-dev/pytest-bdd/pull/534>`_
|
||||
- Step functions can now be decorated multiple times with @given, @when, @then. Previously every decorator would override ``converters`` and ``target_fixture`` every at every application. `#534 <https://github.com/pytest-dev/pytest-bdd/pull/534>`_ `#525 <https://github.com/pytest-dev/pytest-bdd/issues/525>`_
|
||||
|
||||
6.0.1
|
||||
-----
|
||||
- Fix regression introduced in 6.0.0 where a step function decorated multiple using a parsers times would not be executed correctly. `#530 <https://github.com/pytest-dev/pytest-bdd/pull/530>`_ `#528 <https://github.com/pytest-dev/pytest-bdd/issues/528>`_
|
||||
|
|
11
README.rst
11
README.rst
|
@ -208,12 +208,11 @@ for `cfparse` parser
|
|||
from pytest_bdd import parsers
|
||||
|
||||
@given(
|
||||
parsers.cfparse("there are {start:Number} cucumbers",
|
||||
extra_types=dict(Number=int)),
|
||||
parsers.cfparse("there are {start:Number} cucumbers", extra_types={"Number": int}),
|
||||
target_fixture="cucumbers",
|
||||
)
|
||||
def given_cucumbers(start):
|
||||
return dict(start=start, eat=0)
|
||||
return {"start": start, "eat": 0}
|
||||
|
||||
for `re` parser
|
||||
|
||||
|
@ -223,11 +222,11 @@ for `re` parser
|
|||
|
||||
@given(
|
||||
parsers.re(r"there are (?P<start>\d+) cucumbers"),
|
||||
converters=dict(start=int),
|
||||
converters={"start": int},
|
||||
target_fixture="cucumbers",
|
||||
)
|
||||
def given_cucumbers(start):
|
||||
return dict(start=start, eat=0)
|
||||
return {"start": start, "eat": 0}
|
||||
|
||||
|
||||
Example:
|
||||
|
@ -301,7 +300,7 @@ You can implement your own step parser. It's interface is quite simple. The code
|
|||
|
||||
@given(parsers.parse("there are %start% cucumbers"), target_fixture="cucumbers")
|
||||
def given_cucumbers(start):
|
||||
return dict(start=start, eat=0)
|
||||
return {"start": start, "eat": 0}
|
||||
|
||||
|
||||
Override fixtures via given steps
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[[package]]
|
||||
name = "atomicwrites"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
description = "Atomic file writes."
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -36,6 +36,17 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "1.9.0"
|
||||
description = "execnet: rapid multi-Python deployment"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
testing = ["pre-commit"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.7.1"
|
||||
|
@ -236,6 +247,36 @@ tomli = ">=1.0.0"
|
|||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-forked"
|
||||
version = "1.4.0"
|
||||
description = "run tests in isolated forked subprocesses"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
py = "*"
|
||||
pytest = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "pytest-xdist"
|
||||
version = "2.5.0"
|
||||
description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
execnet = ">=1.1"
|
||||
pytest = ">=6.2.0"
|
||||
pytest-forked = "*"
|
||||
|
||||
[package.extras]
|
||||
psutil = ["psutil (>=3.0)"]
|
||||
setproctitle = ["setproctitle"]
|
||||
testing = ["filelock"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
|
@ -341,12 +382,11 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "ecb8acab4af64ddf36f5510f9d99cf462b03d9bdbd18042041dc4d374a44a7ba"
|
||||
content-hash = "a7818e3872ac60220902d85673d411ecfd067f8ec89a6099e4f720c5925e535a"
|
||||
|
||||
[metadata.files]
|
||||
atomicwrites = [
|
||||
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
|
||||
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
||||
{file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
|
||||
|
@ -356,12 +396,21 @@ colorama = [
|
|||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
]
|
||||
distlib = []
|
||||
distlib = [
|
||||
{file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
|
||||
{file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
|
||||
]
|
||||
execnet = [
|
||||
{file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
|
||||
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
|
||||
]
|
||||
filelock = [
|
||||
{file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
|
||||
{file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
|
||||
]
|
||||
glob2 = []
|
||||
glob2 = [
|
||||
{file = "glob2-0.7.tar.gz", hash = "sha256:85c3dbd07c8aa26d63d7aacee34fa86e9a91a3873bc30bf62ec46e531f92ab8c"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
|
||||
{file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
|
||||
|
@ -370,7 +419,10 @@ iniconfig = [
|
|||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
mako = []
|
||||
mako = [
|
||||
{file = "Mako-1.2.1-py3-none-any.whl", hash = "sha256:df3921c3081b013c8a2d5ff03c18375651684921ae83fd12e64800b7da923257"},
|
||||
{file = "Mako-1.2.1.tar.gz", hash = "sha256:f054a5ff4743492f1aa9ecc47172cb33b42b9d993cffcc146c9de17e717b0307"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
|
||||
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
|
||||
|
@ -446,9 +498,17 @@ packaging = [
|
|||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
]
|
||||
parse = []
|
||||
parse-type = []
|
||||
platformdirs = []
|
||||
parse = [
|
||||
{file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"},
|
||||
]
|
||||
parse-type = [
|
||||
{file = "parse_type-0.6.0-py2.py3-none-any.whl", hash = "sha256:c148e88436bd54dab16484108e882be3367f44952c649c9cd6b82a7370b650cb"},
|
||||
{file = "parse_type-0.6.0.tar.gz", hash = "sha256:20b43c660e48ed47f433bce5873a2a3d4b9b6a7ba47bd7f7d2a7cec4bec5551f"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
|
@ -465,16 +525,30 @@ pytest = [
|
|||
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
|
||||
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
|
||||
]
|
||||
pytest-forked = [
|
||||
{file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"},
|
||||
{file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"},
|
||||
]
|
||||
pytest-xdist = [
|
||||
{file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"},
|
||||
{file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
toml = []
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
tox = []
|
||||
tox = [
|
||||
{file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"},
|
||||
{file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
|
||||
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
|
||||
|
@ -501,12 +575,18 @@ typed-ast = [
|
|||
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
|
||||
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
||||
]
|
||||
types-setuptools = []
|
||||
types-setuptools = [
|
||||
{file = "types-setuptools-57.4.18.tar.gz", hash = "sha256:8ee03d823fe7fda0bd35faeae33d35cb5c25b497263e6a58b34c4cfd05f40bcf"},
|
||||
{file = "types_setuptools-57.4.18-py3-none-any.whl", hash = "sha256:9660b8774b12cd61b448e2fd87a667c02e7ec13ce9f15171f1d49a4654c4df6a"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
||||
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
||||
]
|
||||
virtualenv = []
|
||||
virtualenv = [
|
||||
{file = "virtualenv-20.15.1-py2.py3-none-any.whl", hash = "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf"},
|
||||
{file = "virtualenv-20.15.1.tar.gz", hash = "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4"},
|
||||
]
|
||||
zipp = [
|
||||
{file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
|
||||
{file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
|
||||
|
|
|
@ -40,11 +40,13 @@ Mako = "*"
|
|||
parse = "*"
|
||||
parse-type = "*"
|
||||
pytest = ">=5.0"
|
||||
typing-extensions = "*"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
tox = "^3.25.1"
|
||||
mypy = "^0.961"
|
||||
types-setuptools = "^57.4.18"
|
||||
pytest-xdist = "^2.5.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
|
|
@ -9,7 +9,7 @@ import py
|
|||
from mako.lookup import TemplateLookup
|
||||
|
||||
from .feature import get_features
|
||||
from .scenario import find_argumented_step_fixture_name, make_python_docstring, make_python_name, make_string_literal
|
||||
from .scenario import find_argumented_step_function, make_python_docstring, make_python_name, make_string_literal
|
||||
from .steps import get_step_fixture_name
|
||||
from .types import STEP_TYPES
|
||||
|
||||
|
@ -132,9 +132,9 @@ def _find_step_fixturedef(
|
|||
if fixturedefs is not None:
|
||||
return fixturedefs
|
||||
|
||||
argumented_step_name = find_argumented_step_fixture_name(name, type_, fixturemanager)
|
||||
if argumented_step_name is not None:
|
||||
return fixturemanager.getfixturedefs(argumented_step_name, item.nodeid)
|
||||
step_func_context = find_argumented_step_function(name, type_, fixturemanager)
|
||||
if step_func_context is not None:
|
||||
return fixturemanager.getfixturedefs(step_func_context.name, item.nodeid)
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -12,17 +12,16 @@ test_publish_article = scenario(
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import os
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Callable, cast
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import FixtureLookupError, FixtureManager, FixtureRequest, call_fixture_func
|
||||
from _pytest.fixtures import FixtureManager, FixtureRequest, call_fixture_func
|
||||
|
||||
from . import exceptions
|
||||
from .feature import get_feature, get_features
|
||||
from .steps import get_step_fixture_name, inject_fixture
|
||||
from .steps import StepFunctionContext, inject_fixture
|
||||
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -36,98 +35,67 @@ PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
|||
ALPHA_REGEX = re.compile(r"^\d+_*")
|
||||
|
||||
|
||||
def find_argumented_step_fixture_name(name: str, type_: str, fixturemanager: FixtureManager) -> str | None:
|
||||
def find_argumented_step_function(name: str, type_: str, fixturemanager: FixtureManager) -> StepFunctionContext | None:
|
||||
"""Find argumented step fixture name."""
|
||||
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
|
||||
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
|
||||
for fixturedef in fixturedefs:
|
||||
parser = getattr(fixturedef.func, "_pytest_bdd_parser", None)
|
||||
if parser is None:
|
||||
for fixturedef in reversed(fixturedefs):
|
||||
step_func_context = getattr(fixturedef.func, "_pytest_bdd_step_context", None)
|
||||
if step_func_context is None:
|
||||
continue
|
||||
|
||||
match = parser.is_matching(name)
|
||||
if step_func_context.type != type_:
|
||||
continue
|
||||
|
||||
match = step_func_context.parser.is_matching(name)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
parser_name = get_step_fixture_name(parser.name, type_)
|
||||
return parser_name
|
||||
return step_func_context
|
||||
return None
|
||||
|
||||
|
||||
def _find_step_function(request: FixtureRequest, step: Step, scenario: Scenario) -> Callable[..., Any]:
|
||||
"""Match the step defined by the regular expression pattern.
|
||||
|
||||
:param request: PyTest request object.
|
||||
:param step: Step.
|
||||
:param scenario: Scenario.
|
||||
|
||||
:return: Function of the step.
|
||||
:rtype: function
|
||||
"""
|
||||
name = step.name
|
||||
try:
|
||||
# Simple case where no parser is used for the step
|
||||
return request.getfixturevalue(get_step_fixture_name(name, step.type))
|
||||
except FixtureLookupError as e:
|
||||
try:
|
||||
# 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)
|
||||
if argumented_name:
|
||||
return request.getfixturevalue(argumented_name)
|
||||
raise e
|
||||
except FixtureLookupError as e2:
|
||||
raise exceptions.StepDefinitionNotFoundError(
|
||||
f"Step definition is not found: {step}. "
|
||||
f'Line {step.line_number} in scenario "{scenario.name}" in the feature "{scenario.feature.filename}"'
|
||||
) from e2
|
||||
|
||||
|
||||
def _execute_step_function(
|
||||
request: FixtureRequest, scenario: Scenario, step: Step, step_func: Callable[..., Any]
|
||||
request: FixtureRequest, scenario: Scenario, step: Step, context: StepFunctionContext
|
||||
) -> None:
|
||||
"""Execute step function.
|
||||
|
||||
:param request: PyTest request.
|
||||
:param scenario: Scenario.
|
||||
:param step: Step.
|
||||
:param function step_func: Step function.
|
||||
:param example: Example table.
|
||||
"""
|
||||
kw = {"request": request, "feature": scenario.feature, "scenario": scenario, "step": step, "step_func": step_func}
|
||||
"""Execute step function."""
|
||||
kw = {
|
||||
"request": request,
|
||||
"feature": scenario.feature,
|
||||
"scenario": scenario,
|
||||
"step": step,
|
||||
"step_func": context.step_func,
|
||||
"step_func_args": {},
|
||||
}
|
||||
|
||||
request.config.hook.pytest_bdd_before_step(**kw)
|
||||
kw["step_func_args"] = {}
|
||||
|
||||
# Get the step argument values.
|
||||
converters = context.converters
|
||||
kwargs = {}
|
||||
args = get_args(context.step_func)
|
||||
|
||||
try:
|
||||
# Get the step argument values.
|
||||
converters = step_func._pytest_bdd_converters
|
||||
kwargs = {}
|
||||
for arg, value in context.parser.parse_arguments(step.name).items():
|
||||
if arg in converters:
|
||||
value = converters[arg](value)
|
||||
kwargs[arg] = value
|
||||
|
||||
for parser in step_func._pytest_bdd_parsers:
|
||||
if not parser.is_matching(step.name):
|
||||
continue
|
||||
for arg, value in parser.parse_arguments(step.name).items():
|
||||
if arg in converters:
|
||||
value = converters[arg](value)
|
||||
kwargs[arg] = value
|
||||
break
|
||||
|
||||
args = get_args(step_func)
|
||||
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in args}
|
||||
kw["step_func_args"] = kwargs
|
||||
|
||||
request.config.hook.pytest_bdd_before_step_call(**kw)
|
||||
target_fixture = step_func._pytest_bdd_target_fixture
|
||||
|
||||
# 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)
|
||||
if target_fixture:
|
||||
inject_fixture(request, target_fixture, return_value)
|
||||
|
||||
request.config.hook.pytest_bdd_after_step(**kw)
|
||||
return_value = call_fixture_func(fixturefunc=context.step_func, request=request, kwargs=kwargs)
|
||||
except Exception as exception:
|
||||
request.config.hook.pytest_bdd_step_error(exception=exception, **kw)
|
||||
raise
|
||||
|
||||
if context.target_fixture is not None:
|
||||
inject_fixture(request, context.target_fixture, return_value)
|
||||
|
||||
request.config.hook.pytest_bdd_after_step(**kw)
|
||||
|
||||
|
||||
def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequest) -> None:
|
||||
"""Execute the scenario.
|
||||
|
@ -139,22 +107,23 @@ def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequ
|
|||
"""
|
||||
request.config.hook.pytest_bdd_before_scenario(request=request, feature=feature, scenario=scenario)
|
||||
|
||||
try:
|
||||
# Execute scenario steps
|
||||
for step in scenario.steps:
|
||||
try:
|
||||
step_func = _find_step_function(request, step, scenario)
|
||||
except exceptions.StepDefinitionNotFoundError as exception:
|
||||
request.config.hook.pytest_bdd_step_func_lookup_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, exception=exception
|
||||
)
|
||||
raise
|
||||
_execute_step_function(request, scenario, step, step_func)
|
||||
finally:
|
||||
request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario)
|
||||
for step in scenario.steps:
|
||||
context = find_argumented_step_function(step.name, step.type, request._fixturemanager)
|
||||
if context is None:
|
||||
exc = exceptions.StepDefinitionNotFoundError(
|
||||
f"Step definition is not found: {step}. "
|
||||
f'Line {step.line_number} in scenario "{scenario.name}" in the feature "{scenario.feature.filename}"'
|
||||
)
|
||||
request.config.hook.pytest_bdd_step_func_lookup_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, exception=exc
|
||||
)
|
||||
raise exc
|
||||
step_func_context = context
|
||||
|
||||
|
||||
FakeRequest = collections.namedtuple("FakeRequest", ["module"])
|
||||
try:
|
||||
_execute_step_function(request, scenario, step, step_func_context)
|
||||
finally:
|
||||
request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario)
|
||||
|
||||
|
||||
def _get_scenario_decorator(
|
||||
|
|
|
@ -36,18 +36,30 @@ def given_beautiful_article(article):
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, TypeVar
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import FixtureDef, FixtureRequest
|
||||
from typing_extensions import Literal
|
||||
|
||||
from .parsers import StepParser, get_parser
|
||||
from .types import GIVEN, THEN, WHEN
|
||||
from .utils import get_caller_module_locals, setdefault
|
||||
from .utils import get_caller_module_locals
|
||||
|
||||
TCallable = TypeVar("TCallable", bound=Callable[..., Any])
|
||||
|
||||
|
||||
@dataclass
|
||||
class StepFunctionContext:
|
||||
name: str
|
||||
type: Literal["given", "when", "then"]
|
||||
step_func: Callable[..., Any]
|
||||
parser: StepParser
|
||||
converters: dict[str, Callable[..., Any]] = field(default_factory=dict)
|
||||
target_fixture: str | None = None
|
||||
|
||||
|
||||
def get_step_fixture_name(name: str, type_: str) -> str:
|
||||
"""Get step fixture name.
|
||||
|
||||
|
@ -107,7 +119,7 @@ def then(
|
|||
|
||||
|
||||
def _step_decorator(
|
||||
step_type: str,
|
||||
step_type: Literal["given", "when", "then"],
|
||||
step_name: str | StepParser,
|
||||
converters: dict[str, Callable] | None = None,
|
||||
target_fixture: str | None = None,
|
||||
|
@ -125,22 +137,25 @@ def _step_decorator(
|
|||
converters = {}
|
||||
|
||||
def decorator(func: TCallable) -> TCallable:
|
||||
parser_instance = get_parser(step_name)
|
||||
parsed_step_name = parser_instance.name
|
||||
|
||||
def lazy_step_func() -> TCallable:
|
||||
return func
|
||||
|
||||
lazy_step_func._pytest_bdd_parser = parser_instance
|
||||
|
||||
setdefault(func, "_pytest_bdd_parsers", []).append(parser_instance)
|
||||
func._pytest_bdd_converters = converters
|
||||
func._pytest_bdd_target_fixture = target_fixture
|
||||
parser = get_parser(step_name)
|
||||
parsed_step_name = parser.name
|
||||
|
||||
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type)
|
||||
|
||||
def step_function_marker() -> None:
|
||||
return None
|
||||
|
||||
step_function_marker._pytest_bdd_step_context = StepFunctionContext(
|
||||
name=fixture_step_name,
|
||||
type=step_type,
|
||||
step_func=func,
|
||||
parser=parser,
|
||||
converters=converters,
|
||||
target_fixture=target_fixture,
|
||||
)
|
||||
|
||||
caller_locals = get_caller_module_locals()
|
||||
caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(lazy_step_func)
|
||||
caller_locals[fixture_step_name] = pytest.fixture(name=fixture_step_name)(step_function_marker)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
@ -177,7 +192,8 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
|
|||
request.addfinalizer(fin)
|
||||
|
||||
# inject fixture definition
|
||||
request._fixturemanager._arg2fixturedefs.setdefault(arg, []).insert(0, fd)
|
||||
request._fixturemanager._arg2fixturedefs.setdefault(arg, []).append(fd)
|
||||
|
||||
# inject fixture value in request cache
|
||||
request._fixture_defs[arg] = fd
|
||||
if add_fixturename:
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
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
|
||||
|
||||
|
||||
def test_string_steps_dont_take_precedence(testdir):
|
||||
"""Test that normal steps don't take precedence over the other steps."""
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
arguments=textwrap.dedent(
|
||||
"""\
|
||||
Feature: Step precedence
|
||||
Scenario: String steps don't take precedence over other steps
|
||||
Given I have a foo with value 42
|
||||
When pass
|
||||
Then pass
|
||||
"""
|
||||
),
|
||||
)
|
||||
testdir.makeconftest(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
from pytest_bdd import given, when, then, parsers
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
|
||||
@given(parsers.re(r"^I have a foo with value (?P<value>.*?)$"))
|
||||
def _(value):
|
||||
dump_obj("re")
|
||||
|
||||
|
||||
@then("pass")
|
||||
@when("pass")
|
||||
def _():
|
||||
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("I have a foo with value 42")
|
||||
def _():
|
||||
dump_obj("str")
|
||||
return 42
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
[which] = collect_dumped_objects(result)
|
||||
assert which == "re"
|
|
@ -4,6 +4,8 @@ Check the parent givens are collected and overridden in the local conftest.
|
|||
"""
|
||||
import textwrap
|
||||
|
||||
from pytest_bdd.utils import collect_dumped_objects
|
||||
|
||||
|
||||
def test_parent(testdir):
|
||||
"""Test parent given is collected.
|
||||
|
@ -58,42 +60,49 @@ def test_parent(testdir):
|
|||
result.assert_outcomes(passed=1)
|
||||
|
||||
|
||||
def test_global_when_step(testdir, request):
|
||||
def test_global_when_step(testdir):
|
||||
"""Test when step defined in the parent conftest."""
|
||||
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
global_when=textwrap.dedent(
|
||||
"""\
|
||||
Feature: Global when
|
||||
Scenario: Global when step defined in parent conftest
|
||||
When I use a when step from the parent conftest
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
testdir.makeconftest(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd import when
|
||||
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
@when("I use a when step from the parent conftest")
|
||||
def global_when():
|
||||
pass
|
||||
|
||||
def _():
|
||||
dump_obj("global when step")
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
subdir = testdir.mkpydir("subdir")
|
||||
|
||||
subdir.join("test_library.py").write(
|
||||
testdir.mkpydir("subdir").join("test_global_when.py").write(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd.steps import get_step_fixture_name, WHEN
|
||||
from pytest_bdd import scenarios
|
||||
|
||||
def test_global_when_step(request):
|
||||
assert request.getfixturevalue(
|
||||
get_step_fixture_name("I use a when step from the parent conftest",
|
||||
WHEN,
|
||||
)
|
||||
)
|
||||
"""
|
||||
scenarios("../global_when.feature")
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
[collected_object] = collect_dumped_objects(result)
|
||||
assert collected_object == "global when step"
|
||||
|
||||
|
||||
def test_child(testdir):
|
||||
"""Test the child conftest overriding the fixture."""
|
||||
|
@ -198,7 +207,6 @@ def test_local(testdir):
|
|||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd import given, scenario
|
||||
from pytest_bdd.steps import get_step_fixture_name, GIVEN
|
||||
|
||||
|
||||
@given("I have an overridable fixture", target_fixture="overridable")
|
||||
|
@ -215,19 +223,6 @@ def test_local(testdir):
|
|||
def test_local(request):
|
||||
assert request.getfixturevalue("parent") == "local"
|
||||
assert request.getfixturevalue("overridable") == "local"
|
||||
|
||||
|
||||
fixture = request.getfixturevalue(
|
||||
get_step_fixture_name("I have a parent fixture", GIVEN)
|
||||
)
|
||||
assert fixture() == "local"
|
||||
|
||||
|
||||
fixture = request.getfixturevalue(
|
||||
get_step_fixture_name("I have an overridable fixture", GIVEN)
|
||||
)
|
||||
assert fixture() == "local"
|
||||
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import textwrap
|
||||
|
||||
from pytest_bdd.utils import collect_dumped_objects
|
||||
|
||||
|
||||
def test_step_function_multiple_target_fixtures(testdir):
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
target_fixture=textwrap.dedent(
|
||||
"""\
|
||||
Feature: Multiple target fixtures for step function
|
||||
Scenario: A step can be decorated multiple times with different target fixtures
|
||||
Given there is a foo with value "test foo"
|
||||
And there is a bar with value "test bar"
|
||||
Then foo should be "test foo"
|
||||
And bar should be "test bar"
|
||||
"""
|
||||
),
|
||||
)
|
||||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
import pytest
|
||||
from pytest_bdd import given, when, then, scenarios, parsers
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
scenarios("target_fixture.feature")
|
||||
|
||||
@given(parsers.parse('there is a foo with value "{value}"'), target_fixture="foo")
|
||||
@given(parsers.parse('there is a bar with value "{value}"'), target_fixture="bar")
|
||||
def _(value):
|
||||
return value
|
||||
|
||||
@then(parsers.parse('foo should be "{expected_value}"'))
|
||||
def _(foo, expected_value):
|
||||
dump_obj(foo)
|
||||
assert foo == expected_value
|
||||
|
||||
@then(parsers.parse('bar should be "{expected_value}"'))
|
||||
def _(bar, expected_value):
|
||||
dump_obj(bar)
|
||||
assert bar == expected_value
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
[foo, bar] = collect_dumped_objects(result)
|
||||
assert foo == "test foo"
|
||||
assert bar == "test bar"
|
|
@ -1,40 +0,0 @@
|
|||
"""Test when and then steps are callables."""
|
||||
|
||||
import textwrap
|
||||
|
||||
|
||||
def test_when_then(testdir):
|
||||
"""Test when and then steps are callable functions.
|
||||
|
||||
This test checks that when and then are not evaluated
|
||||
during fixture collection that might break the scenario.
|
||||
"""
|
||||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
import pytest
|
||||
from pytest_bdd import given, when, then
|
||||
from pytest_bdd.steps import get_step_fixture_name, WHEN, THEN
|
||||
|
||||
@when("I do stuff")
|
||||
def do_stuff():
|
||||
pass
|
||||
|
||||
|
||||
@then("I check stuff")
|
||||
def check_stuff():
|
||||
pass
|
||||
|
||||
|
||||
def test_when_then(request):
|
||||
do_stuff_ = request.getfixturevalue(get_step_fixture_name("I do stuff", WHEN))
|
||||
assert callable(do_stuff_)
|
||||
|
||||
check_stuff_ = request.getfixturevalue(get_step_fixture_name("I check stuff", THEN))
|
||||
assert callable(check_stuff_)
|
||||
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.assert_outcomes(passed=1)
|
Loading…
Reference in New Issue