Lesson 15 - BDD with Cypress
I’d like to use BDD with Cypress to test the OWASP Juice Shop.
Cypress BDD configuration
I found some of the online advice didn’t quite work for this project, so here are the steps I went through:
Install the cypress-cucumber-preprocessor dependency:
npm install --save-dev cypress-cucumber-preprocessor
npm install --save-dev @types/cypress-cucumber-preprocessor
Add some step definition path config to package.json:
"cypress-cucumber-preprocessor": {
"nonGlobalStepDefinitions": false,
"stepDefinitions": "test/cypress/integration"
}
Update cypress.config.ts to include Cucumber config:
...
import * as otplib from 'otplib'
const browserify = require('@cypress/browserify-preprocessor');
const cucumber = require('cypress-cucumber-preprocessor').default;
const resolve = require('resolve');
const options = {
...browserify.defaultOptions,
typescript: resolve.sync('typescript', { baseDir: "/test/cypress/integration/" }),
};
export default defineConfig({
projectId: '3hrkhu',
defaultCommandTimeout: 10000,
retries: {
runMode: 2
},
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: '**/*.feature',//'test/cypress/e2e/**.spec.ts',
downloadsFolder: 'test/cypress/downloads',
fixturesFolder: false,
supportFile: 'test/cypress/support/e2e.ts',
setupNodeEvents (on: any) {
on("file:preprocessor", cucumber(options)),
on('task', {
...
BDD implementation in layers
Under the cypress folder, add integration and create a feature file integration/basic.feature:
Feature: Basic navigation
Scenario: Open Juice Shop
Given Haxxor goes to the Juice Shop
When she selects "Apple Juice"
Then she can see the details include "The all-time classic"
We want clean separation between the step definitions and the UI implementation, so create a PageObject file `integration/pages/HomePage.cy.js’:
class HomePage {
navigate() {
cy.visit('/#/');
}
openProduct(productName) {
cy.get("mat-grid-tile").contains(productName).click();
}
productStrapLine() {
return cy.get("app-product-details");
}
}
const homePage = new HomePage();
export default homePage;
The step definitions need to go in a folder that matches the feature name, e.g. integration/basic/basic.js:
import { Given, When, Then } from "cypress-cucumber-preprocessor/steps";
import homePage from "../pages/HomePage.cy";
Given("Haxxor goes to the Juice Shop", () => {
homePage.navigate();
});
When('she selects {string}', juiceName => {
homePage.openProduct(juiceName);
});
Then('she can see the details include {string}', juiceDetails => {
expect(homePage.productStrapLine().contains(juiceDetails));
});
So our file structure looks like this:
Run the BDD scenario
Now when you run npx cypress open, you only see the feature file(s):
Note that the test run still uses the Before/After steps defined for the main Cypress e2e tests:
And we can still step through the test:
BDD implementation of an OWASP challenge solution
Let’s solve the Score Board challenge as a BDD Scenario integration/score_board.js:
Feature: Score Board
Scenario: Hidden Score Board can be opened
Given Haxxor goes to the Juice Shop
When she opens the Score Board
Then she sees she has solved 1 challenge
The first step was used before, so we move its definition to integration/common/steps.js.
Define a PageObject integration/pages/ScoreBoardPage.cy.js:
class ScoreBoardPage {
navigate() {
cy.visit('/#/score-board');
}
getSolvedCount() {
return cy.get(".score");
}
}
const scoreBoardPage = new ScoreBoardPage();
export default scoreBoardPage;
And create the other step definitions in integration/score_board/steps.js:
import { When, Then } from "cypress-cucumber-preprocessor/steps";
import scoreBoardPage from "../pages/ScoreBoardPage.cy";
When("she opens the Score Board", () => {
scoreBoardPage.navigate();
});
Then("she sees she has solved {int} challenge/challenges", solvedCount => {
expect(scoreBoardPage.getSolvedCount().contains(" " + solvedCount + "/"));
});
Our file structure is now:
And we have tests for 2 features:
And the new test passes:
Run in CI
We can trigger a test run as a Github Action whenever we push changes to the tests (or application):
name: "CI/CD Pipeline"
on:
push:
branches: [ "master" ]
paths-ignore:
- '*.md'
- 'LICENSE'
- 'monitoring/grafana-dashboard.json'
- 'screenshots/**'
tags-ignore:
- '*'
pull_request:
paths-ignore:
- '*.md'
- 'LICENSE'
- 'data/static/i18n/*.json'
- 'frontend/src/assets/i18n/*.json'
env:
NODE_DEFAULT_VERSION: 20
jobs:
e2e:
runs-on: $
strategy:
matrix:
os: [ubuntu-latest]
browser: [chrome]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: "Use Node.js"
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af #v4.1.0
with:
node-version: $
- name: "Install application"
run: npm install
- name: "Execute end-to-end tests on Ubuntu"
if: $
uses: cypress-io/github-action@v6
with:
install: false
browser: $
start: npm start
wait-on: http://localhost:3000
I implemented this as a matrix in case I want to use other browsers or OSes at any point.
The results can be seen in the run console:
...
────────────────────────────────────────────────────────────────────────────────────────────────────
Running: score_board.feature (2 of 2)
Score Board
info: Solved 1-star scoreBoardChallenge (Score Board)
info: Cheat score for tutorial scoreBoardChallenge solved in 1min (expected ~1min) with hints allowed: 0.42679999999999996
✓ Hidden Score Board can be opened (1526ms)
1 passing (2s)
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Tests: 1 │
│ Passing: 1 │
│ Failing: 0 │
│ Pending: 0 │
│ Skipped: 0 │
│ Screenshots: 0 │
│ Video: false │
│ Duration: 1 second │
│ Spec Ran: score_board.feature │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ✔ basic.feature 00:01 1 1 - - - │
├────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ✔ score_board.feature 00:01 1 1 - - - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
✔ All specs passed! 00:03 2 2 - - -
And in the run summary: