Give AlbumentationsX a star on GitHub — it powers this leaderboard

Star on GitHub

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