Some time ago I started to work on a new product and we decided to do the tests with BDD. I learned some lessons from the experience of doing real world BDD tests. Designing good BDD steps is especially an ongoing process. In this blog article I want to share some insight I gained.
This article is for readers already familiar with the basics of BDD and Gherkin.
Short steps for easy scenario variations
If your steps do just a single simple action, it is easy to combine them to new scenarios: you can achieve variations of scenarios without writing new step implementations.
Take for example a web site where you can add comments to articles, reply to comments, and modify comments and replies. One scenario could be:
[code language="plain"]
Feature: ...
Scenario: Reply to edited comment
...
When I log in as Arthur Dent
And I add a comment to the first article with the text
"""
New comment
"""
And I edit the first comment with the new text
"""
Edited comment
"""
And I reply to the first comment with the text
"""
Reply 1
"""
...[/code]
All these steps are doing exactly one logical user action. So if I not only want to test that replying to an edited comment works, but also verify the behaviour of editing a comment that has replies, then I don't need any additional step definitions - juggling around the existing ones is enough:
[code language="plain"]
Feature: ...
Scenario: Modify a comment with replies
...
When I log in as Arthur Dent
And I add a comment to the first article with the text
"""
New comment
"""
And I reply to the first comment with the text
"""
Reply 1
"""
And I edit the first comment with the new text
"""
Edited comment
"""
...
[/code]
Long steps to keep scenarios short
You can use steps that do complex actions to keep your scenarios short. This is especially useful for setup code.
Just look at the following example:
[code language="plain"]
Feature: ...
Scenario: ...
Given the server is running
And there is an article with the text
"""
This is article 1.
"""
And there is an article with the text
"""
This is article 2.
"""
And the user Dirk Gently is logged in
When I add a comment to first article
Then the comment is displayed under the article
[/code]
The setup code is way longer than the test that is done. Which makes it harder to grasp the intention of the test. So if we add a step that does more things at once, then we get an easy to grasp scenario:
[code language="plain"]
Feature: ...
Scenario: ...
Given the server is running with 2 articles
And the user Dirk Gently is logged in
When I add a comment to first article
Then the comment is displayed under the article
[/code]
In Squish it is currently not possible to call other steps from within a step definition (in Cucumber, this is possible, e.g.). This might change in the future. But it is not really a big limitation since you can do the real work in a script function and the different step implementations simply call these functions.
Separate action (When) and outcome (Then)
At beginning the different step types (Given
, When
, and Then
) might be confusing. But it is useful to get them right, especially the distinction between When
and Then
: use When
for the action you want to challenge in your test. But don't verify the outcome in the When
step. Rather do it in the Then
step.
This separation allows you to check different outcomes of the same action, like checking successful and error conditions.
As an example I want to test the REST API of our web site for modifying comments of articles:
[code language="plain"]
Feature: ...
Scenario: Modify a comment throuh REST API
Given the server is running with 1 article
And the first article has a comment by Arthur Dent
When I authenticate via the REST API as Arthur Dent
And modify the first comment via REST API with the text
"""
Edited comment
"""
Then the reply of the REST command is success
And the first comment has the text
"""
Edited comment
"""
[/code]
And I want to also to test that an error is thrown when you try to modify the comment of a different user:
[code language="plain"]
Feature: ...
Scenario: Error when trying to modify a comment of another user throuh REST API
Given the server is running with 1 article
And the first article has a comment by Dirk Gently
When I authenticate via the REST API as Arthur Dent
And modify the first comment via REST API with the text
"""
Edited comment
"""
Then the reply of the REST command is an error with message
"""
You are not authorized to modify the comment
"""
And the first comment has not changed
[/code]
The difference here is that during the setup, the comment was done by a different user. The When
steps are exactly the same as in the previous scenario. But the outcome (Then
steps) are different. So the clear separation of the setup, the actions, and the outcome, it is possible to effectively reuse the steps.
Use step variations to get readable feature files
One of the big advantages of Gherkin-style BDD testing is that the feature files are human readable. And not only this, they can also be written by the project stakeholders (i.e. non-developers). But this means you should not force any artificial wording for the steps and rather allow variations for the same step.
For example, if your tests checks the number of items that are found, you could do it with a step with a simple pattern:
[code language="plain"]
Feature: ...
Scenario: ...
...
Then the result shows 0 item(s)
Scenario: ...
...
Then the result shows 1 item(s)
Scenario: ...
...
Then the result shows 23 item(s)[/code]
So one step definition (in Python) would be sufficient:
[code language="python"]@Then("the result shows |integer| item(s)")
def step(context, numberOfItems):
...[/code]
But this approach forces the writer of the features to use this very rigid scheme. A nicer feature file would be something along the following:
[code language="plain"]
Feature: ...
Scenario: ...
...
Then the result shows no items
Scenario: ...
...
Then the result shows 1 item
Scenario: ...
...
Then the result shows 23 items[/code]
You could now start figuring out a regular expression that matches all of these. But that is overly complex - just use 3 step definitions (and a shared function that implements the common check):
[code language="python"]def checkNumberOfItemsInResult(numberOfItems):
...
@Then("the result shows no items")
def step(context):
checkNumberOfItemsInResult(0)
@Then("the result shows 1 item")
def step(context):
checkNumberOfItemsInResult(1)
@Then("the result shows |integer| items")
def step(context, numberOfItems):
checkNumberOfItemsInResult(numberOfItems)[/code]
And if a colleague adds a new scenario that looks like:
[code language="plain"]
Feature: ...
Scenario: ...
...
Then the result is empty[/code]
Then adding support for this new step is as simple as:
[code language="python"]@Then("the result is empty")
def step(context):
checkNumberOfItemsInResult(0)[/code]
Use same step definition for different step types
The step definition in Squish is done for a specific step type, for example
[code language="python"]@When("I log in as |any|")
def step(context, name):
...[/code]
This matches the step
[code language="plain"]
Feature: ...
Scenario: ...
...
When I log in as Arthur Dent
...[/code]
But it does not match the step
[code language="plain"]
Feature: ...
Scenario: ...
Given I log in as Arthur Dent
...[/code]
While this is generally preferred, I found it useful in some cases where to use the same step definition in different step types (namely Given
and When
). So instead of using @When(...)
for defining the step definition, simply use @Step(...)
and the step definition matches all step types:
[code language="python"]@Step("I log in as |any|")
def step(context, name):
...[/code]
Just be aware that this also matches Then
step types. This is less useful since the Then
steps do the verification but Given
and When
just perform actions (if the types are used as intended).