Blog

How to write Gherkins compatible with BDD

Under the BDD methodology, you may face the challenge of writing understandable Gherkins for BDD that is compatible with test suites while still maintaining complexity. You’re not alone in this struggle – here are some recommendations to help streamline the process. 

Given, When, or Then?

Determining the appropriate step to write can be challenging. Is it a precondition, input value, validation value, or actual action to be tested? Consider the following conditions for each category.

Given

The ‘Given’ step serves three purposes: 

(1) to set up a pre-condition such as ‘X Service is running’ or ‘Service is subscribed to the queue’, 

(2) to describe an input or existing value such as ‘A valid message’ or ‘An employee exists in the database for the license A000000’, and 

(3) to specify any variations of an input value as a Given, such as ‘The valid message should have an empty license’.

When

This set of instructions provides guidance on how to create clear, concise test scenarios.

  • To test the code, define a step specifically for calling the code. For example: “The service executes the activity logic” or “The post endpoint is called.”
  • Isolate the action to be tested. Do not mix up input values with actions. Move inputs to Given, and leave the action on When. For example, “The controller receives an input model” should be a Given, and the controller action should be left alone under When.
  • Try to limit each scenario to a single action. If multiple actions are needed, create different scenarios or move some of them to Givens.

Then

After executing the required actions, it is important to verify that all necessary conditions have been met. This includes validating log messages, any created entries, and published messages. It is crucial to consider if your validation can be confirmed; if the service shuts down, there is no way to know under the BDD suite scope. If it is mandatory for product, you can add this kind of validation, which will be documented through BDD and in the code. Any unverifiable condition can be passed or ignored.

Example

Let’s look at the following example:

Given the service is running And it has successful subscribed to the queue When a valid message is published to queue And the message does not contains a BatchId Then the service shall create a new BatchId (Type: GUID)

As you can see, the previous Gherkin can be easily understood with a small read–it looks good. The scenario specifies that if the input message does not contain a BatchId, then the service should create one.

If you translate this to the BDD suite, there are a couple of problems that you will be facing. Let’s analyze each step:

Given the service is running And it has successfully subscribed to queue

Given preconditions, the service should be running and listening to a queue 👍

When a valid message is published to queue

Here is when things might get confusing in the BDD suite code. Since the input message and the publishing action are in the same sentence/step, there is no easy way to create a valid message and call the code to be tested. You could just create a message on the fly and send it, but what if you want to give more detail about the message, or maybe some input values for different scenarios?

Let’s split this into a Given and a When:

Given a valid message When the service consumes a message from the queue

Now we can add more detail to the message. By adding a separate step, we can add more ‘Given’ steps to change the input value. Our ‘When’ step is more generic and can be used in multiple scenarios for valid or invalid messages. 

Next Step?

And the message does not contains a BatchId

As you can see, this is a variation from the input message. At this point in the test execution, there is no way to update the input message since it has already been consumed in the previous ‘When’ step. Moving this to the ‘Given’ steps allows you to update the BatchId before processing the message.

Given the service is running And it has successfully subscribed to queue Given a valid message And the message does not contain a BatchId When the service consumes a message from the queue domain.route.queue Then the service shall create a new BatchId (Type: GUID)

Finally, we can complete the Gherkin by adding the ‘Then’ validation to check if the service is creating the BatchId.

But what about that (Type: GUID)? 

Tools for writing Gherkins for BDD

Let’s look at some tools we can use to best describe a valid message with a real guide, or to have the same scenario run with different values.

Data Tables

Let’s check our previous scenario. Given a valid message, this step is open to interpretation. Using DataTables, we can define what is a valid message and which values should be used as an example.

Usually, providing this kind of specification would require a verbose and big Gherkin like this:

Given a valid message with State NY and BatchId A70A0294-D7D0-4BB8-9B6E-68A382170248

First of all, that big GUID does not look good and is likely not understandable. Plus, if we need to add more properties, the whole Gherkin will become unreadable.

With Data Tables, we can use multiple properties and specific values. Just add your data definition below the step.

Given a valid message | property   | value   | | State      | NY         | | BatchId    | A70A0294-D7D0-4BB8-9B6E-68A382170248 |

Background

Let’s say that we have multiple scenarios. Following our previous feature, we can have a scenario providing the BatchId and another without BatchId to be generated by the service.

Scenario: Gather all active employees and emit request to MyExternalService  Given the MyService is running  And it has successful subscribed to domain.route.queue  And a valid message  | property | value   |  | State    | NY       |  | BatchId  | A70A0295-D7D0-4BB8-9B6E-68A382170248 |  When the service consumes a message from the queue domain.route.queue  Then MyService shall call the OtherService for all active employees for the given state  And foreach employee emit a event to the queue domain.route.OutQueue Scenario: Create a BatchId if one is not provided  Given the MyService is running  And it has successful subscribed to domain.route.queue  And a valid message  | property | value |  | State    | NY    |  | BatchId  |       |  When the service consumes a message from the queue domain.route.queue  Then the service shall create a new BatchId (Type: GUID)

As you can see, both scenarios share the same first two steps. In order to avoid the duplicity of repeating the same text over and over for 2, 3, 5, and 10 scenarios, we can use the Background to define steps that must be shared by multiple scenarios.

Background: Given the MyService is running And it has successful subscribed to domain.route.queue Scenario: Gather all active employees and emit request to MyExternalService  And a valid message  | property | value   |  | State    | NY       |  | BatchId  | A70A0295-D7D0-4BB8-9B6E-68A382170248 |  When the service consumes a message from the queue domain.route.queue  Then MyService shall call the OtherService for all active employees for the given state  And foreach employee emit a event to the queue domain.route.OutQueue Scenario: Create a BatchId if one is not provided  And a valid message without BatchId  | property | value |  | State    | NY    |  | BatchId  |       |  When the service consumes a message from the queue domain.route.queue  Then the service shall create a new BatchId (Type: GUID)

Using the scenario’s background will save a lot of space in tickets and the reading of every scenario will become easier, avoiding seeing the same thing multiple times.

Scenario Outline

Have you ever duplicated the same scenario just to add different behavior for different values? As an example, let’s add a new feature to detect activity based on different record properties:

Background:  Given the MyService is running  And receives a message in domain.route.request.queue     | property        | value  | | Id              | A70A0294-D7D0-4BB8-9B6E-68A385670249 | | RequestId       | A70A0294-D7D0-4BB8-9B6E-68A385670278 | | State           | NY                                   | | Dob             | 01/01/1980                           | | LastName        | Last                                 | | FirstName       | First                                | | MiddleName      | Middle                               | | SSN             | ssn                                  | | ZipCode         | zip                                  | | EndContractDate | 01/01/2021                           | | Status          | VALID                                | Scenario: Receive Request. Activity State Change detection  And the previous employee has different State than the current employee  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an Audit entity to domain.route.audit.queue   Scenario: Receive Request. Activity LastName Change detection  And the previous employee has different LastName than the current employee  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue   Scenario: Receive Request. Activity FirstName Change detection  And the previous employee has different FirstName than the current employee  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue Scenario: Receive Request. Activity SSN Change detection  And the previous employee has different SSN than the current employee  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue   Scenario: Receive Request. Activity EndContractDate single detection  And the current employee has expired EndContractDate date  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue   Scenario: Receive Request. Activity Status single detection  And the current employee has NOT VALID Status  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue

As you can see, we have 6 different gherkins for BDD to describe all the possible scenarios to determine activity. First of all, this ticket requires heavy reading and is hard to understand. Imagine the ticket in a planning meeting with multiple members reading a lot of scenarios! This would require multiple minutes of analysis.

Now, what if you need to cover the behavior for specific values for the previous and the current employee? We could create a step for each previous and current specification, like the following:

Background:  Given the MyService is running  And receives a message in domain.route.request.queue  ...   Scenario: Receive Request. Activity State Change detection  And the previous employee has NY State  And the current employee has CA State  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue   Scenario: Receive Request. Activity LastName Change detection  And the previous employee has LAST LastName  And the current employee has LASTY LastName  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue   Scenario: Receive Request. Activity FirstName Change detection  And the previous employee has FIRST FirstName  And the current employee has FIRSTY FirstName  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue Scenario: Receive Request. Activity SSN Change detection  And the previous employee has 123456 SSN  And the current employee has 123455 SSN  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue   Scenario: Receive Request. Activity EndContractDate single detection  And the previous employee does not exist  And the current employee has expired EndContractDate date  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue   Scenario: Receive Request. Activity Status single detection  And the previous employee does not exist  And the current employee has NOT VALID Status  When MyService logic determines activity  Then the service shall scribe the reasons to the audit datastore  And submit an audit entity to domain.route.audit.queue

Now you have specific scenarios. What if, in the last scenario, you want to compare other status values EXPIRED-VALID, LIMITED-EXPIRED, VALID-EXPIRED? You’ll need to duplicate the scenario for all the possible combinations, making the ticket unreadable with 15, 20, and 30 scenarios. Here is where Scenario Outline can be handy.

Scenario: Receive Request. Activity Found And the previous employee has  as And the current employee has  as When MyService logic determines activity Then the service shall scribe the  to the audit datastore And submit an audit entity to domain.route.audit.queue Examples: | previousProperty | previousValue | currentProperty | currentValue | reasons          |                                                                                                           | State            | NY            | State           | CA           | CHANGE_DETECTED  |                                        | LastName         | LAST          | LastName        | LASTY        | CHANGE_DETECTED  | | FirstName        | FIRST         | LastName        | FIRSTY       | CHANGE_DETECTED  | | SSN              | 123456        | SSN             | 123455       | CHANGE_DETECTED  |                                 | EndContractDate  |               | EndContractDate | 01/01/2022   | CONTRACT_EXPIRED | | EndContractDate  | 01/01/2022    | EndContractDate | 01/01/2023   | CHANGE_DETECTED  | | Status           |               | Status          | EXPIRED      | IVALID_STATUS    | | Status           | EXPIRED       | Status          | VALID        | CHANGE_DETECTED  | | Status           | VALID         | Status          | SUSPENDED    | CHANGE_DETECTED  |

Move the specific data samples to a table and adjust values and properties in the Gherkin, encapsulating the step text in <> characters. This way, we can simplify multiple scenarios in a single Gherkin and extend our feature to cover more data scenarios without affecting the complexity of the ticket.

For example, if any employee status is NOT_FOUND, activity should be detected. All we need to do to cover this scenario is add a new row to the examples table.

| Status          |               | Status          | NOT_FOUND    | IVALID_STATUS   |

Need something else?

Data Tables, Background, and Scenario Outlines are the most useful tools when you need to create Gherkins, but there are some other tools available that can be handy when you work for BDD. In the next blog of this series, we will apply some of them using Specflow in .NET. 

 

Stay tuned! 🙂

 

____________________________________________________________________________

 

References: 

Specflow Documentation, Gherkin Reference

Ready to be Unstoppable? Partner with Gorilla Logic, and you can be.

TALK TO OUR SALES TEAM