markdown icon behavior-driven-design.md

Behavior-Driven Design

My Idealised BDD Workflow

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:

  1. Requirements and acceptance criteria are defined in Gherkin files. These are human-readable and can be understood by both technical and non-technical people.
  2. Integration tests are written against the Gherkin files, and they are linked and run using the Cucumber Test Runner.
  3. A feature is deemed complete when all the tests are passing and no previous tests have broken

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.

Gherkin Files

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.

Feature

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

Scenario/Scenario Outline

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

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

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

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 |

Quick Tip

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.


Integration Tests

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.


Quick Tip

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.


Acceptance

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 !