Behavior Driver Development with Python: A Humble Introduction

Behavior Driven Development (BDD) is a process where software is developed starting from the functional definition of the product. This means that not only technical staff is involved in the process from the beginning, but also QA and Business participants may join this process in order to provide a better approach to our product.

Behavior Driven Development (BDD) is a process where software is developed starting from the functional definition of the product. This means that not only technical staff is involved in the process from the beginning, but also QA and Business participants may join this process in order to provide a better approach to our product.

Meet the Three Amigos

To describe how the product should work, it may be difficult for non-technical staff to write things like tests for describing the expected behavior. Instead, there are some DSLs like Gherkin that help formalize the scenarios that describe our product scope.

In practice, BDD is an extension of Test-Driven Development as both processes start by defining tests firsts. From the tests, developers shall implement the product so the tests are executed successfully without errors. TDD also differs from BDD in their target scope. TDD may include tests at any level (unit, integration, acceptance), and BDD is mainly scoped at Acceptance Testing, due to its behavioral approach.

Your first BDD tutorial: Developing a Salt shaker!

An Awesome Salt-Shaker!

Your favorite restaurant asks you to develop a salt shaker that contains by default an amount of salt doses. This shaker shall help customers to perform a single action:

Just like there are customer that don’t put salt on their plates, there are others that love salty plates, so think of scenarios where users take more than one dose from their plates.

… just one more thing. Don’t think on features like “Refilling the shaker” or stuff like that. Only think about Serving feature for this time.

Along this article, a source code repository can be cloned or downloaded. Check it out! And remember that contributions are kindly accepted.

Developer Background

This article assumes readers have a minimum knowledge on software development using Python language. Topics such as modules installation and virtual environments management are not covered within this article. In case you are not familiar with them, check this article about Python set-up for developers I wrote in Datacamp site.

Behavioral tests using Gherkin starts by defining feature files. These files group a single feature from our product, enclosing all the possible related scenarios. For our Shaker project, we could think on the simplest scenario:

The simplest case, usually called Happy Path usually occurs without errors nor abnormal results. Other cases shall be considered as well, describing scenarios not covered by the happy path:

  • Shaking the shaker more than once will result in multiple doses on my plate.
  • Shaking an empty shaker will result in no dose on my plate.

Using Gherkin language, our Happy Path could be expressed as follows:

Don’t worry about the other sections. We’ll cover them along the following sections 😉

For Python developers, there a some options that allow writing tests specified in Gherkin language. Among these options, the most famous are listed below:

For this article we will use pytest-bdd as our choice for writing the tests.

Using pytest-bdd we will write a module test_serving.py where we will write the test functions. Note that features and tests directories are separated. This means that the *.feature and test files are not necessarily within the same directory.

The first thing a test file shall specify is declare the feature file and the covered scenarios. We can do that through the use of the scenario or scenarios functions in pytest-bdd:

The code shown above declared that this module will cover all the scenarios within the serving.feature file. For covering specific scenarios, make use of the method. Note that both methods can be used as function calls or decorators!

Introducing Step Definitions

Once the test module is linked to the feature file, test steps are associated to code using the givenwhen and then decorators. For our example, the single serving scenario definitions are written as follows:

Note that we have written three functions, each one mapping to a step definition using the same text that describes the step in the feature file. Note that we have added another pytest.fixture decorator the the when function, as we will use the returned value of the when function in the final then step definition.

Et voilá. We have coded the tests for our first scenario!

Running our First Test

As pytest-bdd is a plugin for pytest module, the way we run our test is as follows:

Even though pytest execution can be launched using the pytest command, we will launch them using python -m pytest for adding the salty module to the sys.path entries. You can read more about pytest python path settings in pytest official documentation.

When executing the previous command, pytest will discover test modules within the tests directory and perform the checks as declared in our step definition functions. The command output is the following:

As mentioned above, testing the happy path only checks part of your code. It is usually required to test edge scenarios where nominal behaviour changes.

For our salt shaker module, an edge scenario could be shaking an empty shaker. The result shall be serving no dose on a food plate. We could express that feature as follows:

Based on the given scenario definition, we could write our tests functions as follows…

As you can see, we have not implemented the “I Shake it once” step defined, as it was previously defined for the first scenario. Furthermore, we will go over a set of improvements we can apply to our step definitions in order get a simpler version of our tests.

Reuse Step Definitions

Taking into account the defined test scenarios (Single Service and Empty Shaker), there are some steps that are strictly related:

Now, instead of two different given step definitions, we have only one with a step argument as follows:

In a similar manner, the then step where the doses served can be check can be merged into a step with arguments. This could be the gherkin code for both scenarios:

And so the step definitions can be re-implemented as follows:

Pytest-BDD provides a parser object for processing step arguments and injecting them into the functions that require them. By default these arguments are processed as String objects, but some formatting can be specified based on the parse module. Furthermore, step arguments can consists on regular expressions or custom objects!

Check the step_2 in the code repository to see how the tests and feature file result after these changes. As an assignment to test your knowledge of this technique, I suggest you to try adding a step to the Single Service Scenario that checks the salt shaker has 99 remaining doses (hint: And The shaker has 99 units), and merge it with the “It’s empty!” step defined in the Empty shaker scenario.

Maybe not you, but when I put some salt in a plate, I shake the salt shaker more than once. We will write another scenario to describe this use case:

Starting with the given scenario, we could write our step definitions for the given scenario with the following code:

Plus, all the serving scenarios could be merged into one single step!

The new scenario seems good, but not enough. The choice of specifying 10 shakes instead of another amount seems arbitrary. Furthermore, in case we want to add similar scenarios with different shakes, is it a good idea to just copy and paste the scenario? This task becomes tedious and repetitive, and it the long term leads to an unmaintainable state.

Introducing Scenario Outlines

Gherkin allows to define templates for running the same scenario multiple times with different combinations of values. These templates are usually called Scenario Outlines.

On a given scenario, the template parameters are written between brackets < >. The parameter values for each tests are written inside and Examples table:

Use scenario outline tables whenever it is possible

Did you noticed that? We are testing a new edge case where more shakes than remaining doses are performed! Think about the last example 😉

Upon the Scenario definition shown above, a total of 5 tests will be executed, performing all the described checks.

Similarly to the template arguments seen in previous sections, Scenario templates require parsing the input values into proper types. Unfortunately, this case is not as simple as reusing the parsers object seen previously. Instead, converters shall be defined within the test module for a given scenario (or for all of them).

Specifying type converters

The final code for our feature testing will be as follows:

Our final testing module

Notice that Step definition decorators had to be written twice. This is due to the regular and template Scenarios that share the same syntax. But in the real world, this is not a common thing.

When executing the tests suite the following output shall be shown:

 Test execution succeeded!

As mentioned above, we defined 3 Scenarios, but a total of 7 test items have been executed. This is due to the Scenario outline conversion to five tests items, one per entry in the Examples table.

Andrés Pizarro

I am an enthusiastic software engineer who loves coding, especially with Python. Currently I'm working alongside Data Scientists, trying to help them achieve the skills for better software development and create tools to facilitate their work. In a past life, I worked developing satellite control centers for telecommunications companies. I also design stickers for my teammates!

More Posts

Follow Me:
TwitterLinkedIn