Why pytest-bdd?

Introduction

This blog post doesn't advocate for BDD testing. If you are already convinced that you need some BDD tests as part of your software development lifecycle, and you have been looking around for what frameworks to choose, than this post might be useful for you.

Why use pytest-bdd?

I have used a lot of BDD frameworks in the past. The ones that I remember are Lettuce, Freshen and Behave. All of them worked ok. They did the job. Behave for example, is very feature reach, it offers features like data tables which is particularly useful for building data driven tests.

But each of those frameworks came in short in some aspects. I am not going to focus on a feature by feature comparison on all these frameworks. Instead, I am going to share what I would like to see in a testing framework and how pytest-bdd measures against that.

I am looking for a testing framework that:

  • it's simple
  • it's easy to run
  • encourages explicit behaviour rather than implicit
  • has an active support community
  • comes with good documentation
  • supports running scenarios in parallel

And here is how pytest-bdd does on all the points above.

Simple to set up

Step 1. Install the package. (Let's assume you are using poetry.)

poetry add pytest-bdd

Step 2. Create your test structure.

tests
   features
      demo.feature
   __init__.py
   test_demo.py

Step 3. Create a test feature with one scenario.

Feature:
  Scenario: Run a demo for the blog
    Given I have a blog with 2 posts
    When I publish a new post
    Then my blog will contain 3 posts

^^ demo.feature

Step 4. Create the test that runs the feature. Implement the scenario steps.

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

scenarios("features/demo.feature")

@given(parsers.parse("I have a blog with {number_of_posts} posts"))
def given_blog(number_of_posts):
    ...

@when("I publish a new post")
def when_publish():
    ...

@then(parsers.parse("my blog will contain {number_of_posts} posts"))
@then(parsers.parse("my blog will contain {number_of_posts} post"))
def check_blog_posts(number_of_posts):
    ...

^^ test_demo.py

This is it! You have everything you need to start implementing your first test.

Easy to run

Tests written with pytest-bdd take advantage of the pytest runner, and the autodiscovery of tests that it provides. Run your test with pytest (This assumes pytest-bdd was installed with poetry.): poetry run pytest This will automatically find and run all tests in files with the prefix test_ in the tests directory.

Explicit behaviour rather than implicit

I like that pytest-bdd makes the state management of the steps obvious and explicit. There is no context object that is passed implicitly in every step. pytest-bdd makes use of pytest fixtures to manage state and fixtures are explicit, modular and scalable. Here is an example of a fixture passed from one step into the next.

@dataclasses.dataclass
class Blog:
    posts: int = 0

@pytest.fixture
def blog():
    ...

@given(parsers.parse("I have a blog with {number_of_posts} posts"), target_fixture="blog")
def given_blog(number_of_posts):
    return Blog(posts=int(number_of_posts))

@when("I publish a new post")
def when_publish(blog):
    blog.posts += 1

@then(parsers.parse("my blog will contain {number_of_posts} posts"))
@then(parsers.parse("my blog will contain {number_of_posts} post"))
def check_blog_posts(number_of_posts, blog):
    assert blog.posts == int(number_of_posts)

^^ test_demo.py

The fixture blog is overriden in the given_blog step, then passed into the next when_publish step and re-used in the check_blog_posts step for verification. The state is defined explicitly, you know which fixture you are targeting or modifying with each step.

Community and Documentation

The pytest-bdd codebase and documentation are frequently updated. There are quite a few issues logged and discussed, and sometimes workarounds are proposed. There are also a lot of video and written tutorials published online or how to use the framework.

Supports Running Scenarios in parallel

pytest-bdd integrates with pytest-xdist.

Install pytest-dist (Let's assume you are using poetry.):

poetry add pytest-xdist

If you want to distribute tests across multiple CPUs in order to optimise test execution, run multiple worker processes:

poetry run pytest -n auto

Summary

If you are looking for a BDD framework that you want to start using in minutes and especially if you are already using the feature rich pytest framework, give pytest-bdd a try. It's the framework that allows you to write steps in the most pythonic way and you can run all your tests with just one command.