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 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 , read our previous article.
Cypress testing framework
Cypress 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
-
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
-
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" } }
-
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.
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; }, }, });
-
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:
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:
-
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
-
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"); });
-
Run Cypress test with npx cypress open and run our test:
Quick result for a short test:
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"|
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"
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.
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.
Tips and tricks
- Before starting to build a BDD framework for E2E tests it is recommended you become familiar with cypress-cucumber-preprocessor 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 and Allure .
- 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 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.