This is Part 2 in a series. You can find the first part here.
Let’s dive deeper into my idealised Behavior-Driven Design (BDD) workflow. BDD takes Test-Driven Development (TDD) to its logical extent, closely coupling the tests with the requirements. The key features of my BDD workflow are as follows:
As this series is specifically about using AI to improve a BDD process my intention is to not dive too deeply into these explanations. That said let’s look at these steps one by one. If you are already familiar with them, please feel free to skip to the next heading.
Also known as feature files, these are written using the Gherkin syntax and saved using the file name .feature
. A Gherkin file is made up of a single keyword followed by an action for the test/tester to perform or a description of the current state of the application. You can find full documentation for them here, but we will look at the primary keywords now.
Every file begins with the Feature
keyword, which provides a high-level summary of the feature along with a paragraph describing it in more detail:
Feature: Product List
As a small business owner placing an order
I want to view and navigate the product list
So that I can see all available products and their details
A Scenario describes a specific function of this feature, it signifies the beginning of a test. A good scenario will cover only one function. There is a second keyword, Scenario Outline, which provides a mechanism for you to run the same test multiple times with different data using an Example table, which is described below.
Scenario: User can view the product page
...
Steps provide instructions on how to set up the test, which actions to perform while testing, and what the final state should look like. The three main steps are Given
, When
, Then
. To improve readability, you can substitute And
or But
into a sentence, and they will be treated like the preceding keyword. For example in the outline below the keywords And
and But
are both treated as a Then
by Gherkin.
The Given
keyword is where we set up the initial test state, When
describes what actions are performed either by the user or another system, and Then
describes the expected outcome.
Scenario: User can view the product page
Given the user is on page 1 of the product list
Then the user is presented with a paginated list of all available products
And each of the products has a name, a volume, and a cost
But there are no more than 20 products on the page
Examples are essentially an array of data that you loop through, the data is then passed to the Scenario Outline
by replacing placeholders designated like <current-page>
. It provides you with the ability to make your scenarios more generic, allowing them to be run with different data each time. To use them, change your Scenario
keyword to Scenario Outline
, insert the placeholders and table.
Scenario Outline: User can navigate the paginated list
Given the user is on page <current_page> of the product list
When the user clicks the <arrow> pagination arrow
Then the user is moved to page <expected_page> of the product list
Examples:
| arrow | current_page | expected_page |
| left | 2 | 1 |
| right | 1 | 2 |
Tags exist as a way to filter your tests into different categories. I use them for two main purposes: tagging entire Gherkin files so I can run tests on a single feature file and tagging the features I have developed or am developing so my tests are only running only against work that has been done making them both faster and much more readable.
Here is an abbreviated Gherkin file that we will write our tests against.
@product @product-list
Feature: Product List
As a small business owner placing an order
I want to view and navigate the product list
So that I can see all available products and their details
@developed
Scenario: User can view the product page
Given the user is on page 1 of the product list
Then the user is presented with a paginated list of all available products
And each of the products has a name, a volume and a cost
But there are no more then 20 products on the page
@developing
Scenario: User can view product details
Given the user is on page 1 of the product list
When the user clicks on a product in the list
Then they are redirected to the details page for that product
Scenario Outline: User can navigate the paginated list
Given the user is on page <current_page> of the product list
When the user clicks the <arrow> pagination arrow
Then the user is moved to page <expected_page> of the product list
Examples:
| arrow | current_page | expected_page |
| left | 2 | 1 |
| right | 1 | 2 |
ChatGPT has proven to be an effective tool in helping edit and refine feature files it can help ensure that the language is clear and you haven’t overlooked any simple scenarios.
The approach to writing integration tests used by the Cucumber Test Runner is slightly different and may take some getting used to. Rather than your code controlling the flow of the tests, it is controlled by your feature files. You write a series of functions that match the steps in the feature files, and the Cucumber Test Runner takes those steps and runs them in the correct order according to the feature file. Let’s look at an example:
I am using Selenium and Chai, but these can easily be substituted for your WebDriver and assertion library of choice.
@developing
Scenario: User can view product details
Given the user is on page 1 of the product list
When the user clicks on a product in the list
Then they are redirected to the details page for that product
Given(
"the user is on page {int} of the product list",
async function (page: number) {
const pageUrl = `localhost:4000/product-list/${page}`;
await driver.get(pageUrl);
}
);
The sentence structure of our feature file is mostly maintained in the code implementation. The Given
keyword is represented by a function that we invoke. This function is passed two arguments: a step description and an anonymous function holding the test code. You’ll notice within the string, we’ve substituted the numerical value with a placeholder encased in curly braces. This informs Cucumber to pick the number from this position and insert it into the function. In this context, the variable I’ve chosen to hold this number is called page
, and it is assigned a value of 1. Subsequently, we employ the page
variable to navigate us to the first page of the product list.
Let’s look at the rest of the example:
When("the user clicks on a product in the list", async function () {
const product = await driver.findElement(By.className("product-item"));
await product.click();
});
Then(
"they are redirected to the details page for that product",
async function () {
const productDetail = await driver.wait(
until.elementLocated(By.id("product-detail")),
3000
);
expect(
productDetail,
"User should be redirected to the product details page"
).to.exist;
}
);
Here, you can see that the tests for When
and Then
are structured in the exact same way. We have a When
step that clicks on a product, and a Then
step that checks if we have been redirected to the product detail page by searching for the product-detail
ID. One key thing to note is that the Then
function has an assertion. Since Then
is describing an expected outcome, it should always have an assertion.
To make the tags work in your favor, I find it’s best to set up a command in your package.json that runs all the tests: test: "cucumber-js"
, and pass the tags into your npm command like so: npm run test -- --tags "@product-list and @developed"
. The first set of --
tells npm that it should pass the rest of the command to the function it is invoking.
When all the tests are passing it is time for QA to run through the tests manually to ensure that nothing has been missed and to do a final sense check. Once complete the feature is ready to join the larger codebase. It is crucial that your new code is run against all the pre-existing tests to make sure that nothing has been broken and that you add your new tests to the pipeline for all future feature releases.
I am purposely leaving out things like code review, merge strategies and pipeline configurations as this process should be flexible enough to fit in with your current processes.
In part three we are going to look at ways to speed this process up by adding AI into it.
Happy !