pytest-bdd
BDD for pytest
Rank: #2666Downloads: 2,158,931 (30 days)Stars: 1,432Forks: 231
Description
Pytest-BDD: the BDD framework for pytest
========================================
.. image:: https://img.shields.io/pypi/v/pytest-bdd.svg
:target: https://pypi.python.org/pypi/pytest-bdd
.. image:: https://codecov.io/gh/pytest-dev/pytest-bdd/branch/master/graph/badge.svg
:target: https://codecov.io/gh/pytest-dev/pytest-bdd
.. image:: https://github.com/pytest-dev/pytest-bdd/actions/workflows/main.yml/badge.svg
:target: https://github.com/pytest-dev/pytest-bdd/actions/workflows/main.yml
.. image:: https://readthedocs.org/projects/pytest-bdd/badge/?version=stable
:target: https://readthedocs.org/projects/pytest-bdd/
:alt: Documentation Status
pytest-bdd implements a subset of the Gherkin language to enable automating project
requirements testing and to facilitate behavioral driven development.
Unlike many other BDD tools, it does not require a separate runner and benefits from
the power and flexibility of pytest. It enables unifying unit and functional
tests, reduces the burden of continuous integration server configuration and allows the reuse of
test setups.
Pytest fixtures written for unit tests can be reused for setup and actions
mentioned in feature steps with dependency injection. This allows a true BDD
just-enough specification of the requirements without maintaining any context object
containing the side effects of Gherkin imperative declarations.
.. _behave: https://pypi.python.org/pypi/behave
.. _pytest-splinter: https://github.com/pytest-dev/pytest-splinter
Install pytest-bdd
------------------
::
pip install pytest-bdd
Example
-------
An example test for a blog hosting software could look like this.
Note that pytest-splinter_ is used to get the browser fixture.
.. code-block:: gherkin
# content of publish_article.feature
Feature: Blog
A site where you can publish your articles.
Scenario: Publishing the article
Given I'm an author user
And I have an article
When I go to the article page
And I press the publish button
Then I should not see the error message
And the article should be published
Note that only one feature is allowed per feature file.
.. code-block:: python
# content of test_publish_article.py
from pytest_bdd import scenario, given, when, then
@scenario('publish_article.feature', 'Publishing the article')
def test_publish():
pass
@given("I'm an author user")
def author_user(auth, author):
auth['user'] = author.user
@given("I have an article", target_fixture="article")
def article(author):
return create_test_article(author=author)
@when("I go to the article page")
def go_to_article(article, browser):
browser.visit(urljoin(browser.url, '/manage/articles/{0}/'.format(article.id)))
@when("I press the publish button")
def publish_article(browser):
browser.find_by_css('button[name=publish]').first.click()
@then("I should not see the error message")
def no_error_message(browser):
with pytest.raises(ElementDoesNotExist):
browser.find_by_css('.message.error').first
@then("the article should be published")
def article_is_published(article):
article.refresh() # Refresh the object in the SQLAlchemy session
assert article.is_published
Scenario decorator
------------------
Functions decorated with the `scenario` decorator behave like a normal test function,
and they will be executed after all scenario steps.
.. code-block:: python
from pytest_bdd import scenario, given, when, then
@scenario('publish_article.feature', 'Publishing the article')
def test_publish(browser):
assert article.title in browser.html
.. NOTE:: It is however encouraged to try as much as possible to have your logic only inside the Given, When, Then steps.
Step aliases
------------
Sometimes, one has to declare the same fixtures or steps with
different names for better readability. In order to use the same step
function with multiple step names simply decorate it multiple times:
.. code-block:: python
@given("I have an article")
@given("there's an article")
def article(author, target_fixture="article"):
return create_test_article(author=author)
Note that the given step aliases are independent and will be executed
when mentioned.
For example if you associate your resource to some owner or not. Admin
user can’t be an author of the article, but articles should have a
default author.
.. code-block:: gherkin
Feature: Resource owner
Scenario: I'm the author
Given I'm an author
And I have an article
Scenario: I'm the admin
Given I'm the admin
And there's an article
Using Asterisks in Place of Keywords
------------------------------------
To avoid redundancy or unnecessary repetition of keywords
such as "And" or "But" in Gherkin scenarios,
you can use an asterisk (*) as a shorthand.
The asterisk acts as a wildcard, allowing for the same functionality
without repeating the keyword explicitly.
It improves readability by making the steps easier to follow,
especially when the specific keyword does not add value to the scenario's clarity.
The asterisk will work the same as other step keywords - Given, When, Then - it follows.
For example:
.. code-block:: gherkin
Feature: Resource owner
Scenario: I'm the author
Given I'm an author
* I have an article
* I have a pen
.. code-block:: python
from pytest_bdd import given
@given("I'm an author")
def _():
pass
@given("I have an article")
def _():
pass
@given("I have a pen")
def _():
pass
In the scenario above, the asterisk (*) replaces the And or Given keywords.
This allows for cleaner scenarios while still linking related steps together in the context of the scenario.
This approach is particularly useful when you have a series of steps
that do not require explicitly stating whether they are part of the "Given", "When", or "Then" context
but are part of the logical flow of the scenario.
Step arguments
--------------
Often it's possible to reuse steps giving them a parameter(s).
This allows to have single implementation and multiple use, so less code.
Also opens the possibility to use same step twice in single scenario and with different arguments!
And even more, there are several types of step parameter parsers at your disposal
(idea taken from behave_ implementation):
.. _pypi_parse: http://pypi.python.org/pypi/parse
.. _pypi_parse_type: http://pypi.python.org/pypi/parse_type
**string** (the default)
This is the default and can be considered as a `null` or `exact` parser. It parses no parameters
and matches the step name by equality of strings.
**parse** (based on: pypi_parse_)
Provides a simple parser that replaces regular expressions for
step parameters with a readable syntax like ``{param:Type}``.
The syntax is inspired by the Python builtin ``string.format()``
function.
Step parameters must use the named fields syntax of pypi_parse_
in step definitions. The named fields are extracted,
optionally type converted and then used as step function arguments.
Supports type conversions by using type converters passed via `extra_types`
**cfparse** (extends: pypi_parse_, based on: pypi_parse_type_)
Provides an extended parser with "Cardinality Field" (CF) support.
Automatically creates missing type converters for related cardinality
as long as a type converter for cardinality=1 is provided.
Supports parse expressions like:
* ``{values:Type+}`` (cardinality=1..N, many)
* ``{values:Type*}`` (cardinality=0..N, many0)
* ``{value:Type?}`` (cardinality=0..1, optional)
Supports type conversions (as above).
**re**
This uses full regular expressions to parse the clause text. You will
need to use named groups "(?P<name>...)" to define the variables pulled
from the text and passed to your ``step()`` function.
Type conversion can only be done via `converters` step decorator argument (see example below).
The default parser is `string`, so just plain one-to-one match to the keyword definition.
Parsers except `string`, as well as their optional arguments are specified like:
for `cfparse` parser
.. code-block:: python
from pytest_bdd import parsers
@given(
parsers.cfparse("there are {start:Number} cucumbers", extra_types={"Number": int}),
target_fixture="cucumbers",
)
def given_cucumbers(start):
return {"start": start, "eat": 0}
for `re` parser
.. code-block:: python
from pytest_bdd import parsers
@given(
parsers.re(r"there are (?P<start>\d+) cucumbers"),
converters={"start": int},
target_fixture="cucumbers",
)
def given_cucumbers(start):
return {"start": start, "eat": 0}
Example:
.. code-block:: gherkin
Feature: Step arguments
Scenario: Arguments for given, when, then
Given there are 5 cucumbers
When I eat 3 cucumbers
And I eat 2 cucumbers
Then I should have 0 cucumbers
The code will look like:
.. code-block:: python
from pytest_bdd import scenarios, given, when, then, parsers
scenarios("arguments.feature")
@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start):
return {"start": start, "eat": 0}
@when(parsers.parse("I eat {eat:d} cucumbers"))
def eat_cucumbers(cucumbers, eat):
cucumbers["eat"] += eat
@then(parsers.parse("I should have {left:d} cucumbers"))
def should_have_left_cucumbers(cucumbers, left):
assert cucumbers["start"] - cucumbers["eat"] == left
Example code also shows possibility to pass argument converters which ma