For my current project Fix, I use python and Behave for Behavior Driven Design (BDD). Today I’ll describe my process and the tools I use.
In my initial post about Fix, I already wrote a paragraph or two about BDD: I use it mainly to havethe I/O layers under test, which I can not achieve with unit tests .
Suffice it to say that the acceptance tests I wrote this way already paid off. They threw an error I committed due to sloppy design right back in my face .
The toolsBDD usually is tightly connected to Cucumber , which is what I wanted to use for this project. I came in touch with Cucumber first about a year ago when I joined my current project. While I am not doing any C++ in that project, I learned about a lot of other technologies, many of which I use with Fix today.
There seem to be not too many Cucumber implementations for C++ available. In addition, the implementation of the test driver does not need to be extremely performant, neither is it very complex. So I exchanged two of the main advantages of C++ for faster development by using Python instead (I learned that for the current project, too).
In addition to having Behave as an easy to use Cucumber framework, python also has requests . This is a package that makes it extremely easy to send HTTP queries to a service like Fix.
I have to admit, Fix is not yet truly a REST service, but it strives to be. And yes, I first came in contact with REST services in my current project. Oh, and of course, it is easy to deal with JSON (guess what…) in Python but that is also the case in C++. On the server side, I use the great “JSON for modern C++” library by Niels Lohmann.
The processNow I’ll walk you through the setup currently in action at Fix, and a simple example how I use Cucumber and Behave for development. You can find the Python code and the Cucumber feature files here on GitHub .
The acceptance tests written in BDD fashion test the observable behavior of a system. In the case of Fix, that means they mainly test the responses sent back from the server, and in some cases, the files used as storage.
The basic setup is as follows: Every scenario, i.e. every Given-When-Then sequence, creates an empty temporary directory and starts Fix in it. Depending on the scenario the test driver then adds directories and stored issues so Fix has something to work with. At the end of each scenario, the driver stops Fix again and cleans up the directory, unless a test has failed in which case the files are left intact for debugging purposes. Information like where the temporary directory resides is stored in a context object that is created for each scenario by Behave.
Define a new scenarioLet’s go quickly through the process of adding some functionality. BDD is like TDD when it comes to writing tests first. In this case, the new functionality is showing the details of a single issue. This will be achieved by querying <Fix-server>/issue/{id} , where {id} has to be the ID of the issue. If the issue exists, it should return a JSON object containing the issue details. If not, it should give us a HTTP 404 status.
Let’s write this down quickly in Cucumber:
Feature: show details of a single issue Scenario: Empty repository Given an empty Fix repository When we query the issue with ID 42 Then the response has http code 404 Scenario: Issue does not exist Given a Fix repository with issues | ID | summary | description | | 1 | First issue | Issue number one. | | 7 | A Later issue | There will be more | When we query the issue with ID 4 Then the response has http code 404 Scenario: Issue exists Given a Fix repository with issues | ID | summary | description | | 1 | First issue | Issue number one. | | 7 | A Later issue | There will be more | When we query the issue with ID 7 Then the response has http code 200 And the response is an object | ID | summary | description | | 7 | A Later issue | There will be more |We see three scenarios, each containing a sequence of Given-When-Then steps. Each table belongs to the step in the line before, and the line starting with And in the last scenario is simply a second Then step.
Write the step definitionsAt first, running this will cause Behave to complain about a number of unknown steps, so we need to write step definitions for them. Since it denotes the new functionality we are about to implement, the lines When we query the issue... should be among those unknown steps. Here’s how this particular step is implemented:
@when('we query the issue with ID {issue_id:d}') def step_impl(context, issue_id): context.rest_response = requests.get('http://localhost:8080/issue/' + str(issue_id))It is pretty obvious that this calls the GET method on the endpoint as described above. The result is stored in Behave’s context object so it can be evaluated in the When steps. In case a step contains a table, it is stored in the context object, so it can be evaluated in the step definition. You can see examples of the use in the Fix GitHub repository .
Implement the functionalityHaving implemented the step definitions, the scenarios will fail. It is now time to switch to TDD for the implementation. Often, the unit tests used for TDD will cover behavior similar to that described in the scenarios. This is OK because they describe it with a finer granularity and in a much more technical way.
In the case of Fix, TDD will bring us the logic needed inside the server, but not everything we need to make the Cucumber tests pass. The I/O layers are not under unit tests, so additional functionality may be needed to pass the acceptance tests. In the case of querying a single issue, this will probably be reading the contents of a single file that’s not too complicated.
ConclusionBehave was very easy to set up, and Python is a flexible but powerful language that enables us to write the necessary step definitions quickly and effortless. That way, we can focus on the functionality itself while still having the behavior covered by our feature files.