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: