How to design good BDD steps

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:

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
            """
         ...

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:

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
            """
         ...

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:

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

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:

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

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:

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
            """

And I want to also to test that an error is thrown when you try to modify the comment of a different user:

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

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:

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)

So one step definition (in Python) would be sufficient:

@Then("the result shows |integer| item(s)")
def step(context, numberOfItems):
    ...

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:

Feature: ...

  Scenario: ...
    ...
    Then the result shows no items
    
  Scenario: ...
    ...
    Then the result shows 1 item
    
  Scenario: ...
    ...
    Then the result shows 23 items

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):

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)

And if a colleague adds a new scenario that looks like:

Feature: ...

  Scenario: ...
    ...
    Then the result is empty

Then adding support for this new step is as simple as:

@Then("the result is empty")
def step(context):
    checkNumberOfItemsInResult(0)

Use same step definition for different step types

The step definition in Squish is done for a specific step type, for example

@When("I log in as |any|")
def step(context, name):
    ...

This matches the step

Feature: ...

  Scenario: ...
    ...
    When I log in as Arthur Dent
    ...

But it does not match the step

Feature: ...

  Scenario: ...
    Given I log in as Arthur Dent
    ...

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:

@Step("I log in as |any|")
def step(context, name):
    ...

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).

0 Comments

Leave a reply

Your email address will not be published. Required fields are marked *

*