Blog>>Quality assurance>>Cypress BDD integration for efficient testing

Cypress BDD integration for efficient testing

Behavior-driven development (BDD) is a software development methodology that aims to bridge the gap between business stakeholders and technical teams by emphasizing collaboration and communication. It encourages the creation of human-readable scenarios that describe the desired behavior of a system from the user's perspective. In this article, we explore how integrating Cypress with Cucumber and BDD practices can enhance the testing process by promoting better collaboration, understanding, and documentation of application behavior from a user perspective.

Cypress Cucumber preprocessor

Cucumber      link-icon is an open-source tool for automated tests, such as E2E tests, in behavior-driven development. It allows mapping Gherkin scenarios with their implementation in the code. Having projects with BDD and not using Cucumber is possible, but not utilizing Gherkin scenarios for E2E testing would be untapped potential. Cucumber integrates with various testing frameworks, i.e. Cypress, Selenium or Playwright to automate the validation of these scenarios against the actual application, as well as programming languages such as Ruby, Java, JavaScript, and Python. This connection between natural language descriptions and automated tests helps ensure that development stays aligned with business requirements and enables efficient communication among a project's parties throughout the development life cycle.

If you want to explore behavior-driven development further      link-icon, read our previous article. 

Services test automation

Cypress testing framework

Cypress      link-icon is a modern E2E testing framework (nowadays also used for component testing). It supports both JavaScript and TypeScript for more robust design and coding, offers real-time interactive testing, enables viewing of snapshots of the application state at every step, and debugs issues with help of the commands log. Cypress comes with built-in waiting, retry mechanisms, and a comprehensive dashboard for test results. Cypress with Cucumber is ready to go for BDD implementation and runs E2E tests in the regular way.

Cypress runs inside of the browser, making this approach unique compared to tools like Selenium or Playwright, carrying both pros and cons:

  • Cypress runs in the same JavaScript runtime as the application under test, leading to quicker execution time.
  • Cypress tests are bound to a same-origin policy. Even though the newest versions of Cypress allow for cross-origin tests, they’re still easier with different tools.
  • Even though Cypress is asynchronous, everything is wrapped in specific methods, making advanced asynchronous operations difficult.

Before choosing Cypress as a tool for automated testing it is important to consider these differences, as more suitable options might be available.

Project setup

It is assumed that the reader has a basic understanding of project initialization and adding new dependencies in JavaScript. Having the IDE open at the project’s location and project initialized with npm init, let’s proceed with packages installation. All installed packages have the newest versions at the moment of publication of this article. 

Prerequisites:

  • LTS (long-term support) node installed - this project was built on v18.17.1.

  • Visual Studio Code as IDE (recommended)

  • Extensions in VSC for .feature files and Gherkin syntax (recommended):

    • Material Icon Theme - adding better icons .feature files
    • Cucumber (Gherkin) Full Support - syntax, snippets and formatting
  1. Open terminal at your project location and initialize new repository:

    Run npm init -y 

    After package.json file is created, change its content to:

    {
       "dependencies": {
         "@badeball/cypress-cucumber-preprocessor": "latest",
         "@bahmutov/cypress-esbuild-preprocessor": "latest",
         "cypress": "latest",
         "typescript": "latest"
       },
       "cypress-cucumber-preprocessor": {
         "json": {
           "enabled": false
         },
         "stepDefinitions": [
           "cypress/e2e/step_definitions/\[filepath]/*.{js,ts}",
           "cypress/e2e/common_step_definitions/*.{js,ts}"
         ]
       }
    }
    

    And run npm install to install all required packages.

    At the moment, the latest packages versions are:

    • cypress@12.17.4
    • typescript@5.1.6
    • cypress-cucumber-preprocessor@18.0.4
    • cypress-esbuild-preprocessor@2.2.0
  2. Initialize project:

    Run tsc --init at the same project location.

    After tsconfig.ts file is created, change its content to:

     {
       "compilerOptions": {
         "esModuleInterop": true,
         "moduleResolution": "node16"
       }
     }
    
  3. Opening Cypress for the first time and selecting E2E Testing will create the required folder structure and necessary files.

    Run npx cypress open at the project's location and go through simple setup.

    Cypress setup

    After the first successful run we can close Cypress for now and finish the project configuration. With cypress.config.ts created, replace its config for one recommended by the cypress-cucumber-preprocessor author:

     import { defineConfig } from "cypress";
     import createBundler from "@bahmutov/cypress-esbuild-preprocessor";
     import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor";
     import createEsbuildPlugin from "@badeball/cypress-cucumber-preprocessor/esbuild";
    
     export default defineConfig({
       e2e: {
         specPattern: "\*\*/*.feature",
         async setupNodeEvents(
           on: Cypress.PluginEvents,
           config: Cypress.PluginConfigOptions
         ): Promise<Cypress.PluginConfigOptions> {
           await addCucumberPreprocessorPlugin(on, config);
           
           on(
             "file:preprocessor",
             createBundler({
               plugins: \[createEsbuildPlugin(config)],
             })
           );
    
           return config;
         },
       },
     });
    
  4. Adjusting the file structure to package.json file

Inside cypress/e2e folder create three new folders at the same level:

  • common_step_definitions
  • scenarios
  • step_definitions

In the end, the project’s file structure should look like this:

project’s file structure

Writing tests

In a standard project setup, the user has to have .feature and .ts files both with the same name, but with larger projects this can be problematic. Using stepDefinition config from this article, the user has to create a .feature file and folder with the same name, but inside the folder could be many test files that match our scenario.

Implementation of BDD E2E Cypress tests requires each Gherkin scenario to be mapped with a test by containing the same description inside the Given/When/Then function and then the test logic itself, as below:

Given/When/Then(“Gherkin test description”, () => {\
// test implementation\
})

Running the first test:

  1. Create file CodiLime_contact.feature inside e2e/scenarios folder with content:

    1. Feature: CodiLime contact form
      Scenario: Contact form is visible
        Given I am on a CodiLime page
          And Contact button is visible
        When I click a Contact button
        Then Contact form should be displayed
    
  2. Inside cypress/e2e/step_definitions/ create folder CodiLime_contact with file contact_form.ts with code:

    1. import { Given, When, Then } from "@badeball/cypress-cucumber-preprocessor";
    
    Given("I am on a CodiLime page", () => {
      cy.visit("https://codilime.com/");
    })
    
    Given("Contact button is visible", () => {
      cy.get("a\[data-testid='button-contact-us']").should("be.visible");
    })
    
    When("I click a Contact button", () => {
      cy.get("a\[data-testid='button-contact-us']").click({force: true});
    });
    
    Then("Contact form should be displayed", () => {
      cy.contains("h2", "Contact us").should("be.visible");
      cy.get("form\[id*='hsForm']").should("be.visible");
    });
    
  3. Run Cypress test with npx cypress open and run our test:

    npx cypress open

    Quick result for a short test:

test result

Gherkin scenarios with variables

Gherkin test scenarios allow the use of variables, meaning it is possible to have the same test logic, but with different data. 

.feature file:

Given I have open contact form\
Then I input <firstName> and <lastName>

.ts test file:

import { Given, Then } from "@badeball/cypress-cucumber-preprocessor";

Given("I am on a CodiLime Contact page", () => {
  cy.visit("https://codilime.com/contact/");
})

Then("I input first name {string} and last name {string}", (firstName: string, lastName: string) => {
    cy.get("input\[id*='firstname']").type(firstName);
    cy.get("input\[id*='lastname']").type(lastName);
});

The same goes for using a Scenario outline with examples where the test will repeat itself for each dataset in Examples; in this case it will run three times:

  Scenario Outline: Fill contact form
      Given I am on a CodiLime Contact page
    Then I input <firstName> and <lastName>
      Examples:
\|firstName   | lastName   |
\|"testFirst1"| "testLast1"|
\|"testFirst2"| "testLast2"|
\|"testFirst3"| "testLast3"|
test example

Common scenarios

With a huge amount of written tests, reusable scenarios are very helpful. Moving steps implementation from the step_definitions folder to common_step_definitions without changing any file names, the tests will still pass.

Having repeatable steps implementation available with a global scope is good practice that will save time by not repeating steps like logging in or navigating through the page in the long run.

Creating file login.ts inside cypress/e2e/common_step_definitions and implementing step handling logging in will allow you to use it in every .feature file step and have the logic reused:

import { Given } from "@badeball/cypress-cucumber-preprocessor";



Given("I am logged in into the main page", () => {

  cy.get("#inputLogin").type("tester1")

  cy.get("#inputPassword").type("password123")

  cy.get("#submit").click()

})

This allows you to use every .feature file step and have the logic reused:

Given I am logged in into the main page

Tagging

Tagging is a more advanced feature worth mentioning. Adding @tag to the above scenario in .feature files, will allow you to execute only specific tests by tag. 

Add a new Scenario in CodiLime_contact.feature file:

    @mobile
    Scenario: Contact button is not visible in mobile view
      Given I am on a CodiLime page in mobile view
      And Contact button is not visible

Implementation in contact_form.ts:

import { Given } from "@badeball/cypress-cucumber-preprocessor";

Given("I am on a CodiLime page in mobile view", () => {
  cy.viewport(550, 750);
  cy.visit("https://codilime.com/");
})

Given("Contact button is not visible", () => {
  cy.get("a\[data-testid='button-contact-us']").should("not.be.visible");
})

Running tests with tags requires headless mode as in the Cypress GUI there is no option to run specific tag scenarios only.

Run npx cypress run --env TAGS="@mobile"

npx cypress run --env TAGS="@mobile"

Only one test was run, the one with the tag @mobile, others are marked with a blue color - meaning they are skipped. 

Debugging

Debugging tests erroDebuggingrs, inside the Cypress runner, built in a BDD manner using Cucumber is basically the same as the regular way. Each error that appears in a single test scenario will be printed in the Command Log with an exhaustive description. On top of that, for each step in any scenario, Cypress creates snapshots, which basically works like taking a screenshot after every action in the application. 

run npx cypress

Working with errors that appear in a headless run, for example inside of CI/CD pipelines, similar error descriptions will be printed, providing specific scenarios that failed and the type of error with a detailed description. Additionally, Cypress creates artifacts, which can be stored in any CI/CD runner. Those artifacts are screenshots of the app at the time of an error or a video of a whole failed run, as well as Cypress logs or browser-specific items like console output, cookies, or network requests, which can be very helpful in more difficult situations where the reason for a failed test is not obvious at first glance.

Cypress artifacts

Tips and tricks

  • Before starting to build a BDD framework for E2E tests it is recommended you become familiar with cypress-cucumber-preprocessor      link-icon as it could help to customize it for specific projects.
  • Working with the whole team on creating good Gherkin scenarios will result in shared understanding and clear requirements, as well as early feedback that will save time and money in the long run.
  • Integrate Gherkin scenarios with other tools for easier test management, such as Jira, Zephyr, Xray or TestRail, allowing the business side an easier way to review tests.
    Scenarios also sit in the E2E testing repository, but that might not be the most convenient place for non-technical stakeholders.
  • BDD with Cypress offers a variety of test result outputs which can be further integrated with different tools like paid TestRail or free options like html report      link-icon and Allure      link-icon.
  • Even though Cucumber allows reuse of implemented code by adding the same scenarios in .feature files, it is still recommended to apply the Page Object Model approach. 
  • Implement tags into CI/CD pipelines to optimize which tests should be run for specific needs, instead of running everything in each case.
  • Keep following Cypress good practices and guidelines      link-icon and understand them because if the Cypress team builds the tool according to their ideas on how E2E testing should be done this can sometimes lead to misunderstanding and frustration.

Summary 

The convergence of Cypress and BDD provides a powerful testing approach that crosses technical execution with human readability. By using BDD's Gherkin feature files, the whole team can describe application behavior in a language accessible to everyone involved. This shared experience while creating scenarios that also work as the application’s documentation, enables you to turn natural language into working code with fewer mistakes.

Cypress, with the help of BDD, elevates testing to a collaborative practice that enforces better understanding - communication which ultimately results in more robust applications.

Raszka  Adrian

Adrian Raszka

QA Engineer

Adrian Raszka is a QA Engineer and author on CodiLime's blog. Check out the author's articles on the blog. Read about author >

Read also

Get your project estimate

For businesses that need support in their software or network engineering projects, please fill in the form and we'll get back to you within one business day.