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

Step advanced usecases #548

Merged
merged 24 commits into from
Aug 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Unreleased
- ``parsers.re`` now does a `fullmatch <https://docs.python.org/3/library/re.html#re.fullmatch>`_ instead of a partial match. This is to make it work just like the other parsers, since they don't ignore non-matching characters at the end of the string. `#539 <https://github.com/pytest-dev/pytest-bdd/pull/539>`_
- Require pytest>=6.2 `#534 <https://github.com/pytest-dev/pytest-bdd/pull/534>`_
- Using modern way to specify hook options to avoid deprecation warnings with pytest >=7.2.
- Add generic ``step`` decorator that will be used for all kind of steps `#548 <https://github.com/pytest-dev/pytest-bdd/pull/548>`_
- Add ``stacklevel`` param to ``given``, ``when``, ``then``, ``step`` decorators. This allows for programmatic step generation `#548 <https://github.com/pytest-dev/pytest-bdd/pull/548>`_

6.0.1
-----
Expand Down
140 changes: 137 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ Install pytest-bdd
pip install pytest-bdd


The minimum required version of pytest is 4.3.


Example
-------

Expand Down Expand Up @@ -870,6 +867,143 @@ You can learn more about `functools.partial <https://docs.python.org/3/library/f
in the Python docs.


Programmatic step generation
----------------------------
Sometimes you have step definitions that would be much easier to automate rather than writing manually over and over again.
This is common, for example, when using libraries like `pytest-factoryboy <https://pytest-factoryboy.readthedocs.io/>`_ that automatically creates fixtures.
Writing step definitions for every model can become a tedious task.

For this reason, pytest-bdd provides a way to generate step definitions automatically.

The trick is to pass the ``stacklevel`` parameter to the ``given``, ``when``, ``then``, ``step`` decorators. This will instruct them to inject the step fixtures in the appropriate module, rather than just injecting them in the caller frame.

Let's look at a concrete example; let's say you have a class ``Wallet`` that has some amount for each currency:

.. code-block:: python

# contents of wallet.py

import dataclass

@dataclass
class Wallet:
verified: bool

amount_eur: int
amount_usd: int
amount_gbp: int
amount_jpy: int


You can use pytest-factoryboy to automatically create model fixtures for this class:

.. code-block:: python

# contents of wallet_factory.py

from wallet import Wallet

import factory
from pytest_factoryboy import register

class WalletFactory(factory.Factory):
class Meta:
model = Wallet

amount_eur = 0
amount_usd = 0
amount_gbp = 0
amount_jpy = 0

register(Wallet) # creates the "wallet" fixture
register(Wallet, "second_wallet") # creates the "second_wallet" fixture


Now we can define a function ``generate_wallet_steps(...)`` that creates the steps for any wallet fixture (in our case, it will be ``wallet`` and ``second_wallet``):

.. code-block:: python

# contents of wallet_steps.py

import re
from dataclasses import fields

import factory
import pytest
from pytest_bdd import given, when, then, scenarios, parsers


def generate_wallet_steps(model_name="wallet", stacklevel=1):
stacklevel += 1

human_name = model_name.replace("_", " ") # "second_wallet" -> "second wallet"

@given(f"I have a {human_name}", target_fixture=model_name, stacklevel=stacklevel)
def _(request):
return request.getfixturevalue(model_name)

# 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()} in my {human_name}"),
target_fixture=f"{model_name}__amount_{currency}",
stacklevel=stacklevel,
)
def _(value: int) -> int:
return value

@then(
parsers.parse(f"I should have {{value:d}} {currency.upper()} in my {human_name}"),
stacklevel=stacklevel,
)
def _(value: int, _currency=currency, _model_name=model_name) -> None:
wallet = request.getfixturevalue(_model_name)
assert getattr(wallet, f"amount_{_currency}") == value

# Inject the steps into the current module
generate_wallet_steps("wallet")
generate_wallet_steps("second_wallet")


This last file, ``wallet_steps.py``, now contains all the step definitions for our "wallet" and "second_wallet" fixtures.

We can now define a scenario like this:

.. code-block:: gherkin

# contents of wallet.feature
Feature: A feature

Scenario: Wallet EUR amount stays constant
Given I have 10 EUR in my wallet
And I have a wallet
Then I should have 10 EUR in my wallet

Scenario: Second wallet JPY amount stays constant
Given I have 100 JPY in my second wallet
And I have a second wallet
Then I should have 100 JPY in my second wallet


and finally a test file that puts it all together and run the scenarios:

.. code-block:: python

# contents of test_wallet.py

from pytest_factoryboy import scenarios

from wallet_factory import * # import the registered fixtures "wallet" and "second_wallet"
from wallet_steps import * # import all the step definitions into this test file

scenarios("wallet.feature")


Hooks
-----

Expand Down
5 changes: 3 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import os
import sys
from importlib import metadata

sys.path.insert(0, os.path.abspath(".."))

Expand Down Expand Up @@ -51,9 +52,9 @@
# built documents.
#
# The short X.Y version.
version = pytest_bdd.__version__
version = metadata.version("pytest-factoryboy")
# The full version, including alpha/beta/rc tags.
release = pytest_bdd.__version__
release = version

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
Loading