Behavior-Driven Development (BDD) is a really powerful tool that helps us build robust, value-based software. You can sometimes hear detractors say that it brings a lot of complexity and leads to long tests difficult to maintain. Let's take stock of what BDD really is, by determining anti-patterns and best practices.
This sentence is the main symptom of a misunderstanding about Behavior-Driven Development. BDD is not a file, a test or a testing activity. It's a development process: a methodology.
You cannot write a methodology, you apply it. Most of the people believe that having written tests in a Given When Then style means that they do BDD. But we cannot blame them when we can find badly named frameworks such as BDDMockito or Chai BDD. And there are two main reasons why the keyword "BDD" is misused in these cases.
First, it is not because they are labeled BDD that they facilitate or encourage people to work in a BDD fashion. Even worse, they allow them to think they are doing it! Suppose we rename JUnit5 in TDD JUnit, will developers start writing their tests before coding only because of that?
How can a framework ensure that a methodology is applied?
Secondly, Given When Then is a convention, a nomenclature in which we have to write scenarios in natural language, also called Gherkin, if you use Cucumber. This is only a small part of what BDD is.
Behavior-Driven Development, it's all in the name: focus on the behavior of the software and let it drive our development process.
The main advantages: business value first and robustness.
The starting point is always a scenario, defining the needs of the user. Without this, you can not create software that matters. This "specification" must be made with the main stakeholder: the user (or at least one person who represents and understands him).
Taking the need at the source is always the best way. The user (his representative) is not only a stakeholder, he must participate in the design phase of the application. That's why it's essential to use a natural language to define your needs. As a general rule, an end user can not write or review JUnit tests.
It also focuses on the definition and the usage of an ubiquitous language. If the user calls this a "dog", it must be named so in the scenarios. Everyone, from the user to the developer, must speak the same language, it will avoid confusion and facilitate communication, therefore the collaboration, and then speed up the development process.
The scenarios are not just the description of business rules. These rules must be "instanciated" on concrete examples and you can then call them scenarios.
This way, you will identify what are the acceptance criteria (Then) from some "Given" context and/or inputs "When" a specific kind of user performs an action => e.g. how your application should behave in that situation.
Feature: As a cinephile In order to not loose a single minute of a movie I want to know what are the next showings of the movie theater
Background:
Given a cinephile
Scenario: Looking for the next showings before the start of the last one
Given the last showing is not started yet
When the user asks for the next showings
Then the returned showings are not started yet
And the returned showings are not starting in the next 15 minutes
Every scenario must define:
In the above example written in the Cucumber-Gherkin way, you can see that all the terms belong to the lexical fields of the cinema. Cinephile, Movie, Movie Theatre and Projection define the ubiquitous language.
This business language, combined with Gherkin's natural language nomenclature, makes this specification perfectly understandable for our users.
The context is clearly identified: before the start of the last showing. The next showings are the expected results of the previous scenario. You may have also noticed that we have here several levels of abstraction.
The definition of the feature is really high level. Each scenario is a specialization of the functionality in a given context, and the results of the feature are refined in the "then" directives. As you can see, the concept of next showings is refined in the "then" statement: showings that are not yet started and do not start in the next 15 minutes.
Sometimes this refinement does not come from the user himself. You can imagine that the movie theatre - which is also a stakeholder - does not want us to show the user the immediate for some reasons of crowd management.
Or maybe, just to avoid the disturbance of someone entering late a room where a movie has already started. Every stakeholders count, this is one of the principles of the outside-in methodology.
From one type of user to another, the expectations (acceptance criteria) of an action / feature can be very different. You can not bring value if you do not know who the user is. Not identifying the persona may also lead to writing unusable features.
In addition, it also introduces security issues due to a lack of permissions management. BDD is not a security methodology, but it has the very good side effect of encouraging people to think about the type of user that should be able to perform certain actions.
In the example above, the identified persona is a cinephile.
Let's assume our application is also used by the projectionists of the movie theatre. For this type of users, knowing what the next showings will mean which projections they need to run in order to be synchronized with their work schedule.
BDD is a development process based on two loops. In the first loop we identify the user needs that are expressed as scenarios with examples as follow:
Feature: As a cinephile In order to not loose a single minute of a movie I want to know what are the next showings of the movie theater
Background:
Given a cinephile
Scenario: Looking for the next showings before the start of the last one
Given the last showing is not started yet
When the user asks for the next showings
Then the returned showings are not started yet
And the returned showings are not starting in the next 15 minutes
For each step of a scenario, we start writing the fixtures - also called step definitions - i.e. implementing the steps from a technical stand point. These fixtures will constrain and therefore define the behavior of the application.
public class StepDefs implements En {
public StepDefs() {
Given("^a cinephile$", () -> me = new Cinephile("me"));
Given("^the last showing is not started yet$",
() -> lastShowing = new Showing(now().plusHours(3)));
When("^the user asks for the next showings$",
() -> nextShowings = searchShowings.searchNextShowingsFor(me));
Then("^the returned showings are not started yet$", () -> {
assertThat(allShowings.stream().filter(showing -> !showing.hasStarted()))
.containsAnyElementsOf(nextShowings);
});
Then("^the returned showings are not starting in the next 15 minutes$", () -> {
assertThat(nextShowings)
.noneMatch( showing -> showing.startsBefore(now().plusMinutes(15)));
});
}
}
Note that we are not talking about the production code here. The above example is written in a cucumber java8 style.
The scenario with its related fixtures represent an acceptance test.
Then we start the inner TDD loops by implementing the production code used in the step definitions until the functionality is developed. That's why we call this a "double loop".
Behavior-Driven Development is a methodology that has emerged from TDD. It has the same strengths as Test-Driven Development - assures good test coverage and code design - while getting rid of the limitation of the bottom-up approach of TDD.
When working with TDD, it can sometimes be very difficult to determine where to start, what may be the appropriate granularity of the method we want to build. Have you ever had this feeling: after performing a major refactoring during the last TDD iteration, you would have converged faster if you had thought of the problem with a top-down approach? This is where BDD helps a little, by forcing you to think first from the consumer's point of view, you can say this methodology is totally complementary to TDD and, more importantly, it drives your TDD iterations.
This is the basis. BDD offers a real framework for expressing user needs. By integrating a scenario into a user story, you make sure that it has been sufficiently prepared to be developed. Although it is very difficult to find anything in tools such as JIRA, the scenario will ensure the functional documentation of your software which, by design, is automatically checked.
As mentioned earlier, the identification of the persona is the key, so it must be explicitly define inside a Given directive. This will also avoid confusion. Beware that persona are important from a security point of view. It will define the required privileges to trigger a feature.
NOTE: Persona are not defined by fancy user names, rather use roles, it avoids mental mappings.
Your scenario must describe a single responsibility for a feature and you must have one scenario per use case. By not following these rules, you will end up with an accidental complexity.
Remember that a When clause is describing a feature. If you need multiple Whens in a single scenario, it may mean you have a design problem.
Thens are dedicated to the assertions on the outputs of a feature, the user is not a part of it.
the returned showings are not starting in the next 15 minutes
The subject must be the functional object on which we must check the post-conditions of the feature. The passive voice is best to emphasize the functional objects affected by the execution of the feature.
You should never have any user actions here, they are dedicated to Whens directives. Putting a user action in a Then means that you can control what your user will do exactly after running your feature.
We usually start with the happy path to reassure ourselves, but unfortunately the process ends here. A large number of bugs are due to a lack of analyzis around the expected errors. Thinking first about the unhappy paths ensures that your functionality is built robustly. Do not forget to fail fast.
Some Gherkin frameworks allow you to use tables, although this can be very convenient and justified when you want to iterate over several input examples, it is totally inadvisable to use it to represent a complex object because you would lose the functional intent.
In the folowing example:
Given the showing:
| name | schedule | director | rating | duration |
| Gore Movie | 21H00 | Jhon Doe | NC-17 | PT2H5M |
When a child tries to buy a ticket for this showing
Then the purchase is forbidden
A showing is built in the Given using the content of the table. Unfortunately we do not know what characteristics of the showing are important for the rest of the scenario, until we read the When and the Then.
Here, only the rating is important, the goal being to prevent a child from buying a ticket for a movie rated NC-17. A better approach would be:
Given a NC-17 rated showing
When a child tries to buy a ticket for this showing
Then the purchase is forbidden
It is totally fine to hide inside the fixture the initialization of the showing fields as soon as they have no impact on the behavior of the scenario.
Some frameworks such as Cucumber will not fail your scenario by default if one of the steps is not implemented. But what can you say about a passing Scenario in which the only Then directive is not implemented?
Never postpone the implementation of a step definition, it goes against the principles of the double loop first and no one will ever implement it later.
Usually, we discover that an implementation is missing when we try to debug the given functionality. If you use Cucumber, make sure to turn on the strict option.
Most of the time, users split their step definitions in the same way as their scenarios. Unfortunately, it couples everything together and the steps you made cannot be reused for another scenario. The step definitions should be organised in the same way than your domain concepts and, moreover, named after them.
This way your step definitions code will be factorized, easier to find and navigate through, you'll get easier steps to manage and reuse. In other words, the step definitions must be designed with the same processes we use to create our business domain!
And and But are fine for chaining Givens and Thens in a scenario.
However, do not define them as fixtures:
And("^the returned showings are not starting in the next 15 minutes$", ...)
In this case, it is really difficult to understand if this directive can be used as a Given or a Then and you can not usually use the same directive for different purposes. A Given usually initiates the input of a feature while a Then makes assertions on the outputs.
The scenario must be understandable from the point of view of the user. Words must therefore be carefully chosen from the concepts of the business domain. Avoid technical terms!
The natural language enforced by gherkin with the use of an ubiquituous language provides a powerful functional documentation of your software which is always up-to-date. This makes it easy to integrate new members into the team and helps your product owner / user understand what features your software supports.
But do not just keep it in your version control repository, expose it as a static website generated from your CI/CD. You can use tools like Pickles for this purpose.
Some teams lock up an entire afternoon in a room to produce a dozen scenarios needed for software. We do not live in the dark age of Waterfall, it is not necessary to specify everything in advance. Generally, these teams commit these scenarios directly in their repository. This has the disadvantage of confusing what the application is currently doing.
Even adding a tag like @wip to the top of a scenario is counterproductive for two reasons: first, the feature file name is meaningful enough to make someone think that it is really developed; secondly, invariants or simply the understanding of a functionality evolves over time. In some cases, the features are descoped... and your repository is not a backlog ...
A scenario must be written just before implementation - just in time - to save time and money.
Sometimes you can see a scenario connected to the controller of an application and, as you can imagine, it's a bad practice. The scenarios have a business purpose: connecting it to the application's infrastructure level changes them into integration tests, and this methodology is not intended to meet an integration goal.
A scenario is used to describe the use case of a feature. It should not fail because your JSON serializer is not well defined in a controller.
If you are already "implementing" BDD and find yourself with hard-to-maintain tests, it's usually because of this lack of separation of concerns.
Be aware that even Cucumber documentation discourages this usage:
Cucumber is not an API automation tool
In hexagonal architecture, it is generally advisable to connect the scenario directly to the domain modules of your application.
If you like to write integration tests using a Gherkin style, you should take a look at KarateDSL. A really powerful tool with many predefined step definitions that handles some HTTP operations for you.
Here is an example of a KarateDSL feature file:
Feature: Profile endpoint
Scenario: creating a profile
Given path 'profiles'
And request
"""
{ "topics": [{"name": "DDD"},{"name": "Hexagonal Architecture"}],"talksFormats": ["QUICKIE","CONFERENCE"] }
"""
When method post
Then assert responseStatus == 201
And match responseHeaders['Location'] contains only (profilesUrl + userId)
NOTE: All step definitions are built into KarateDSL, which means that the integration test does not require more than this file.
This is a development methodology that helps you first define the behavior of a feature using scenarios with examples. It is complementary to the TDD which takes a bottom-up approach while BDD comes from the top.
The double loop ensure the creation of a robust and tested software and the scenarios provide a living documentation that facilitates communication with non-technological stakeholders.
You can also read this really good article which also cover some other best practices in BDD:
and browse this repository, where you'll find a full implementation of Cucumber and KarateDSL.