In this tutorial, I am going to show you how to start using BDD and will use a Pokémon Pokédex search feature as an example. BDD means Behavior-driven Development and it’s a refinement of existing TDD processes. It considers the feature behavior ahead of the development and the test processes. A system can have many behaviors, which are basically how a feature operates input, process and output.
One of the biggest problems in the software development process is that the different parts are not speaking the same language. Generally the Product Owner describes the feature in one way, the developer develops it in another way and the tester tests it in a brand new way – and in some cases, documentation and tests are skipped because of a deadline. These are common situations and in the end, the real feature and value are not delivered to the end user.
(End user not receiving the feature)
BDD brings a solution so that all parts speak the same language: the features files. To write them we use Gherkin, a business specification language. These files are responsible for handling the application behaviors. Separate behaviors make it easy to develop and test – and in the end, an application is composed of many behaviors working together. But the main benefits are collaboration and automation.
All stakeholders can easily understand the feature file because it is written in natural language and it just needs to follow some rules. Here is an example of a feature file:
Feature: pokédex search As a Pokemon trainer, I want to search for pokemons in my pokédex, So I can learn more about them. Scenario: Name pokédex search Given the pokédex page When the user searches for "Pikachu" Then the "Pikachu" information is shown
Let’s take a look at what each keyword means in this:
Feature is a high level description of the feature itself. It could have a description and it generally is a user story
Scenario is the concrete example of a business rule. It is composed by Given, When and Then and a feature could have many scenarios
Given is the step used to show the initial state of behavior
When is the step used to describe a behavior action or event
Then is the step used to describe the behavior outcome
These keywords were not used in the example, but they are also valid Gherkin syntax:
And/But when you have more than one Given, When or Then you could replace it with an And or But. Something like Given a=1 And b=2 When a + b Then the output is 2. Remember to use this carefully
Background is the step that we can use to run before a scenario, to avoid repeating it over and over.
Scenario Outline is the way you can use variables inside a scenario, so you can parametrize without repeating yourself.
There are still more Gherkin keywords and you can check them all here.
This is the first phase of BDD, we wrote the feature specifications. Now we need to automate it.
In this tutorial we will use pytest-bdd because it brings all the pytest best stuff, like fixture and tags, and we can run unit and functional tests with just one command. If you are familiar with pytest, we highly recommend you to check it out 🙂
Let’s set up the project! We need to create a test directory with a feature/ and a setp_definition/ directores inside it.
We will need a pokedex.feature file inside the features directory and a __init__.py, conftest.py and test_pokedex_steps.py inside steps_definitions directory. After creating all files you should have this structure:
pokedex-pytest-bdd └── tests ├── features │ └── pokedex.feature └── step_definitions ├── __init__.py ├── conftest.py └── test_pokedex_steps.py
You can use this dir structure in a project or a Django app context.
If you want, you can have a bdd dir inside your test dir to make it more decoupled. Our recommendation is to let the feature and step_definition folders in the same depth, to make it easier to find things while debugging.
Inside the pokedex-pytest-dbb folder, we can create a virtualenv and install pytest and selenium:
python -m venv venv source venv/bin/activate pip install pytest pytest-bdd requests selenium
For selenium to work properly, you will need to download and add the geckodriver to your path. You can find it here.
We’re going to create our features file and use some Gherkin tags that we have learned 🙂
Feature: pokédex search As a Pokemon trainer, I want to search for pokemons in my pokédex, So I can learn more about them. Background: Given the pokédex page Scenario Outline: Name pokédex search When the user searches for "<text>" Then the "<pokemon>" information is shown Examples: Pokemons | text | pokemon | | Pikachu | Pikachu | | Charmander | Charmander |
And now that we have our feature, we can automate it!
First, create a fixture inside conftest.py. It will be used to handle our selenium webdriver and will be available to all tests. We just need to pass the browser as an argument to the test function – pytest cool stuff.
import pytest from selenium import webdriver @pytest.fixture def browser(): browser = webdriver.Firefox() browser.implicitly_wait(10) yield browser browser.quit()
Pytest does not allow us to run the feature file directly. So, in order to be able to find the correct scenario, we need to specify it at the beginning, with the scenarios function.
Now we need to create for each Given, When and Then a function that will handle the steps to reproduce it.
In pytest-bdd, we do it by decorating the function with @given(), @when(), @then() and passing the text as an argument.
We are using selenium to simulate the user interaction with the page and pytest to assert the results. The assert will be always in that function, and the complete file should look like this :
from pytest_bdd import scenarios, given, when, then from selenium.webdriver.common.keys import Keys scenarios('../features/pokedex.feature') @given('the pokedex page') def pokedex_page(browser): browser.get('https://pokedex.org/') @when('the user searches for "<text>"') def search_pokemon_name(browser, text): search_input = browser.find_element_by_id('monsters-search-bar') search_input.send_keys(text, Keys.ENTER) @then('the "<pokemon>" informations are shown') def results_have_one(browser, pokemon): results = browser.find_elements_by_id('monsters-list') assert len(results) > 0 assert pokemon in results.text
To run it, you need to type “pytest” inside your terminal with the virtual environment activated. We are using the -v tag (verbose), so pytest shows us a more descriptive message.
And the result should be:
(venv) ➜ pokedex-pytest-bdd pytest -v ================================= test session starts ==================================== platform linux -- Python 3.8.2, pytest-5.4.3, py-1.8.1, pluggy-0.13.1 -- /pokedex-pytest-bdd/venv/bin/python cachedir: .pytest_cache rootdir: /pokedex-pytest-bdd plugins: bdd-3.4.0 collected 2 items .../test_pokedex_steps.py::test_name_pokédex_search[Pikachu-Pikachu] PASSED [ 50%] .../test_pokedex_steps.py::test_name_pokédex_search[Charmander-Charmander] PASSED [100%] ==================================== 2 passed in 26.42s ==================================
And all passed, we did it!
You can pass a @given as a param to @when or @then functions, because it is a fixture. And you can have more than one decorator to a function. This way you can write more descriptive scenarios and reuse the same functions. You just need to decorate the function with all the steps necessary. Something like:
@when('something') @when('something in other words') def function() ...
You can write BDD to unit and API tests, but it’s overengineering it. For these kinds of tests, TDD is better. BDD really shines to end-to-end and UI tests.
The only downside of using pytest-bdd, in my point of view, is that we need to explicitly import all scenarios in the step definition files. It would be great if they were automatically found or if pytest ran the feature files itself, but pytest brings so much to the table that it doesn’t matter.
I hope you have enjoyed this tutorial. Feel free to ping me if you have any questions. And if you want to learn more about BDD, here are some interesting links:
Happy BDD, bye