Merge pull request #534 from pytest-dev/reusable-step-functions

Reusable step functions
This commit is contained in:
Alessio Bogon 2022-07-15 18:24:40 +02:00 committed by GitHub
commit e24aee0028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 385 additions and 194 deletions

View File

@ -1,6 +1,11 @@
Changelog 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 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>`_ - 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>`_

View File

@ -208,12 +208,11 @@ for `cfparse` parser
from pytest_bdd import parsers from pytest_bdd import parsers
@given( @given(
parsers.cfparse("there are {start:Number} cucumbers", parsers.cfparse("there are {start:Number} cucumbers", extra_types={"Number": int}),
extra_types=dict(Number=int)),
target_fixture="cucumbers", target_fixture="cucumbers",
) )
def given_cucumbers(start): def given_cucumbers(start):
return dict(start=start, eat=0) return {"start": start, "eat": 0}
for `re` parser for `re` parser
@ -223,11 +222,11 @@ for `re` parser
@given( @given(
parsers.re(r"there are (?P<start>\d+) cucumbers"), parsers.re(r"there are (?P<start>\d+) cucumbers"),
converters=dict(start=int), converters={"start": int},
target_fixture="cucumbers", target_fixture="cucumbers",
) )
def given_cucumbers(start): def given_cucumbers(start):
return dict(start=start, eat=0) return {"start": start, "eat": 0}
Example: 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") @given(parsers.parse("there are %start% cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start): def given_cucumbers(start):
return dict(start=start, eat=0) return {"start": start, "eat": 0}
Override fixtures via given steps Override fixtures via given steps

108
poetry.lock generated
View File

@ -1,6 +1,6 @@
[[package]] [[package]]
name = "atomicwrites" name = "atomicwrites"
version = "1.4.0" version = "1.4.1"
description = "Atomic file writes." description = "Atomic file writes."
category = "main" category = "main"
optional = false optional = false
@ -36,6 +36,17 @@ category = "dev"
optional = false optional = false
python-versions = "*" 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]] [[package]]
name = "filelock" name = "filelock"
version = "3.7.1" version = "3.7.1"
@ -236,6 +247,36 @@ tomli = ">=1.0.0"
[package.extras] [package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 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]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@ -341,12 +382,11 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "ecb8acab4af64ddf36f5510f9d99cf462b03d9bdbd18042041dc4d374a44a7ba" content-hash = "a7818e3872ac60220902d85673d411ecfd067f8ec89a6099e4f720c5925e535a"
[metadata.files] [metadata.files]
atomicwrites = [ atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
] ]
attrs = [ attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {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-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, {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 = [ filelock = [
{file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
{file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
] ]
glob2 = [] glob2 = [
{file = "glob2-0.7.tar.gz", hash = "sha256:85c3dbd07c8aa26d63d7aacee34fa86e9a91a3873bc30bf62ec46e531f92ab8c"},
]
importlib-metadata = [ importlib-metadata = [
{file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
{file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, {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-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, {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 = [ 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_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, {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-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
] ]
parse = [] parse = [
parse-type = [] {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"},
platformdirs = [] ]
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 = [ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, {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-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, {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 = [ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {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 = [ tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {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 = [ 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_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, {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-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, {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 = [ typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, {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 = [ zipp = [
{file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
{file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},

View File

@ -40,11 +40,13 @@ Mako = "*"
parse = "*" parse = "*"
parse-type = "*" parse-type = "*"
pytest = ">=5.0" pytest = ">=5.0"
typing-extensions = "*"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
tox = "^3.25.1" tox = "^3.25.1"
mypy = "^0.961" mypy = "^0.961"
types-setuptools = "^57.4.18" types-setuptools = "^57.4.18"
pytest-xdist = "^2.5.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View File

@ -9,7 +9,7 @@ import py
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from .feature import get_features 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 .steps import get_step_fixture_name
from .types import STEP_TYPES from .types import STEP_TYPES
@ -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_function(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

View File

@ -12,17 +12,16 @@ test_publish_article = scenario(
""" """
from __future__ import annotations from __future__ import annotations
import collections
import os import os
import re import re
from typing import TYPE_CHECKING, Callable, cast from typing import TYPE_CHECKING, Callable, cast
import pytest 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 . import exceptions
from .feature import get_feature, get_features 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 from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
if TYPE_CHECKING: if TYPE_CHECKING:
@ -36,98 +35,67 @@ 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: def find_argumented_step_function(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()):
for fixturedef in fixturedefs: for fixturedef in reversed(fixturedefs):
parser = getattr(fixturedef.func, "_pytest_bdd_parser", None) step_func_context = getattr(fixturedef.func, "_pytest_bdd_step_context", None)
if parser is None: if step_func_context is None:
continue continue
match = parser.is_matching(name) if step_func_context.type != type_:
continue
match = step_func_context.parser.is_matching(name)
if not match: if not match:
continue continue
parser_name = get_step_fixture_name(parser.name, type_) return step_func_context
return parser_name
return None 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( def _execute_step_function(
request: FixtureRequest, scenario: Scenario, step: Step, step_func: Callable[..., Any] request: FixtureRequest, scenario: Scenario, step: Step, context: StepFunctionContext
) -> None: ) -> None:
"""Execute step function. """Execute step function."""
kw = {
:param request: PyTest request. "request": request,
:param scenario: Scenario. "feature": scenario.feature,
:param step: Step. "scenario": scenario,
:param function step_func: Step function. "step": step,
:param example: Example table. "step_func": context.step_func,
""" "step_func_args": {},
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"] = {}
# Get the step argument values.
converters = context.converters
kwargs = {}
args = get_args(context.step_func)
try: try:
# Get the step argument values. for arg, value in context.parser.parse_arguments(step.name).items():
converters = step_func._pytest_bdd_converters if arg in converters:
kwargs = {} 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} 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
# 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=context.step_func, request=request, kwargs=kwargs)
if target_fixture:
inject_fixture(request, target_fixture, return_value)
request.config.hook.pytest_bdd_after_step(**kw)
except Exception as exception: except Exception as exception:
request.config.hook.pytest_bdd_step_error(exception=exception, **kw) request.config.hook.pytest_bdd_step_error(exception=exception, **kw)
raise 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: def _execute_scenario(feature: Feature, scenario: Scenario, request: FixtureRequest) -> None:
"""Execute the scenario. """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) request.config.hook.pytest_bdd_before_scenario(request=request, feature=feature, scenario=scenario)
try: for step in scenario.steps:
# Execute scenario steps context = find_argumented_step_function(step.name, step.type, request._fixturemanager)
for step in scenario.steps: if context is None:
try: exc = exceptions.StepDefinitionNotFoundError(
step_func = _find_step_function(request, step, scenario) f"Step definition is not found: {step}. "
except exceptions.StepDefinitionNotFoundError as exception: 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=exception request.config.hook.pytest_bdd_step_func_lookup_error(
) request=request, feature=feature, scenario=scenario, step=step, exception=exc
raise )
_execute_step_function(request, scenario, step, step_func) raise exc
finally: step_func_context = context
request.config.hook.pytest_bdd_after_scenario(request=request, feature=feature, scenario=scenario)
try:
FakeRequest = collections.namedtuple("FakeRequest", ["module"]) _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( def _get_scenario_decorator(

View File

@ -36,18 +36,30 @@ def given_beautiful_article(article):
""" """
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, TypeVar from typing import Any, Callable, TypeVar
import pytest import pytest
from _pytest.fixtures import FixtureDef, FixtureRequest from _pytest.fixtures import FixtureDef, FixtureRequest
from typing_extensions import Literal
from .parsers import StepParser, get_parser from .parsers import StepParser, get_parser
from .types import GIVEN, THEN, WHEN 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]) 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: def get_step_fixture_name(name: str, type_: str) -> str:
"""Get step fixture name. """Get step fixture name.
@ -107,7 +119,7 @@ def then(
def _step_decorator( def _step_decorator(
step_type: str, step_type: Literal["given", "when", "then"],
step_name: str | StepParser, step_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,
@ -125,22 +137,25 @@ def _step_decorator(
converters = {} converters = {}
def decorator(func: TCallable) -> TCallable: def decorator(func: TCallable) -> TCallable:
parser_instance = get_parser(step_name) parser = get_parser(step_name)
parsed_step_name = parser_instance.name parsed_step_name = parser.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
fixture_step_name = get_step_fixture_name(parsed_step_name, step_type) 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 = 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 func
return decorator return decorator
@ -177,7 +192,8 @@ def inject_fixture(request: FixtureRequest, arg: str, value: Any) -> None:
request.addfinalizer(fin) request.addfinalizer(fin)
# inject fixture definition # inject fixture definition
request._fixturemanager._arg2fixturedefs.setdefault(arg, []).insert(0, fd) request._fixturemanager._arg2fixturedefs.setdefault(arg, []).append(fd)
# inject fixture value in request cache # inject fixture value in request cache
request._fixture_defs[arg] = fd request._fixture_defs[arg] = fd
if add_fixturename: if add_fixturename:

114
tests/args/test_common.py Normal file
View File

@ -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"

View File

@ -4,6 +4,8 @@ Check the parent givens are collected and overridden in the local conftest.
""" """
import textwrap import textwrap
from pytest_bdd.utils import collect_dumped_objects
def test_parent(testdir): def test_parent(testdir):
"""Test parent given is collected. """Test parent given is collected.
@ -58,42 +60,49 @@ def test_parent(testdir):
result.assert_outcomes(passed=1) 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.""" """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( testdir.makeconftest(
textwrap.dedent( textwrap.dedent(
"""\ """\
from pytest_bdd import when from pytest_bdd import when
from pytest_bdd.utils import dump_obj
@when("I use a when step from the parent conftest") @when("I use a when step from the parent conftest")
def global_when(): def _():
pass dump_obj("global when step")
""" """
) )
) )
subdir = testdir.mkpydir("subdir") testdir.mkpydir("subdir").join("test_global_when.py").write(
subdir.join("test_library.py").write(
textwrap.dedent( textwrap.dedent(
"""\ """\
from pytest_bdd.steps import get_step_fixture_name, WHEN from pytest_bdd import scenarios
def test_global_when_step(request): scenarios("../global_when.feature")
assert request.getfixturevalue( """
get_step_fixture_name("I use a when step from the parent conftest",
WHEN,
)
)
"""
) )
) )
result = testdir.runpytest()
result = testdir.runpytest("-s")
result.assert_outcomes(passed=1) result.assert_outcomes(passed=1)
[collected_object] = collect_dumped_objects(result)
assert collected_object == "global when step"
def test_child(testdir): def test_child(testdir):
"""Test the child conftest overriding the fixture.""" """Test the child conftest overriding the fixture."""
@ -198,7 +207,6 @@ def test_local(testdir):
textwrap.dedent( textwrap.dedent(
"""\ """\
from pytest_bdd import given, scenario 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") @given("I have an overridable fixture", target_fixture="overridable")
@ -215,19 +223,6 @@ def test_local(testdir):
def test_local(request): def test_local(request):
assert request.getfixturevalue("parent") == "local" assert request.getfixturevalue("parent") == "local"
assert request.getfixturevalue("overridable") == "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"
""" """
) )
) )

View File

@ -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"

View File

@ -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)