Front-end tutorial on Behavior Driven Development with Cucumber, Cypress, and Jest

Joe Tannoury
8 min readSep 27, 2021

--

Creating a project without testing its features is fun and all; but have you ever successfully went on onto creating a successful managed and tested project? In this article I will explain the Behavior Driven Development (BDD for short) agile approach with an example project that you can follow step by step.

In this tutorial I won’t go into depth onto the methodologies of BDD but rather the practical steps that should be taken if you’re wanting to start creating a front-end web application using Vue, Cucumber and Cypress and Jest. We will programmingly write the stories of what our client side application is supposed to do with features.

Step 0: Globally installed programs

  • A package manager
    You must have a package manager; npm or yarn. Follow instructions online if you do not have one available.
  • Vue CLI globally (recommend to start off)
    We’ll start by globally installing the Vue CLI-ent. We will use it to create a basic skeleton of our project Vue.js front end application.
    npm install -g @vue/cli
    yarn global add @vue/cli
  • Additional libraries will be installed locally in the project folder itself and managed of course by our package manager. There will be package.json generated that hold’s the versions of the dependencies.

Step 1.0: Project init

In this step we will start by creating our project’s barebones. Navigate in the terminal wherever you’d like your project directory situated. We can do so by writing on our terminal:

vue create appnamehere

Note: the name of the project should be completely in lower case.

This will start the the wizard setup of our project. In my case, for the purpose of this tutorial, I went ahead and named my app attentionbutton.

Setup of the of the creating of the project’s skeleton
commands I’ve used to start off the Vue CLI wizard
all the settings of the preset I’ll use

For the sake of the tutorial, make sure both E2E and Unit Testing are selected; you may as well add or remove features as you please. As you go on with the setup please make sure you select Cypress as the E2E testing solution. Once done, vue-cli will start creating the structure.

Step 1.1: Installing Cucumber

It’s time for us to install Cucumber into our project.

npm install --save-dev @cucumber/cucumber 
# or
yarn add --dev @cucumber/cucumber
  • Now we should configure Cucumber’s settings and make it work with out Cypress. To do so, we’ll install a well used plugin called: cypress-cucumber-preprocessor. To install it:
    npm install --save-dev cypress-cucumber-preprocessor
  • on tests/e2e/plugins/index.js, add on top
    const cucumber = require("cypress-cucumber-preprocessor").default;
    and into the module.exports function add before the return statement: on("file:preprocessor", cucumber())
  • Add support for Cucumber feature files to your Cypress configuration, inside the json file in cypress.json add "testFiles": "**/*.feature"

Finally, to finish configuring the plugin, we’ll in our package.json file:

"cypress-cucumber-preprocessor": {
"nonGlobalStepDefinitions": true",
"stepDefinitions": "tests/e2e/steps"
}

This setting will make all steps non global. This will be true by default in the foreseeable future. A bunch of other settings are available to use @ https://www.npmjs.com/package/cypress-cucumber-preprocessor.

Step 2.0: serve

In this step we’ll go ahead and test run our newly generated project.

Run the script:

  • serve: to run the vue.js application in development mode (with auto refresh)
  • test:unit: to run our Unit tests (Jest)
  • test:e2e: to run the E2E tests (Cypress)

Go ahead and run the basic generated client side app:

npm run serve

You may now visit localhost:8080 and check it out.

It should look like this:

Step 2.1: Running tests

Test run the default unit test with by running the script ‘test:unit’ (npm run test:unit)

To run the E2E tests run the script ‘test:e2e’ (npm run test:e2e). Once initialized the Cypress window should open.

Don’t be alarmed, we installed Cucumber in our project, and only will .feature files to appear here.

Step 3: Writing tests

I won’t go into much detail about what user stories are; however they’re essential in the BDD process. It consists of writing the case scenarios of what your project is supposed to do. We will be writing these using Gherkin syntax (by using Cucumber).

The idea of the example project I have in mind is a website where you can press a button, and when you press it you gain points. You’re able to press it as long as its not disabled. The more you press it the more points you get. However, if you press too many times in a certain amount of time the button will reject you for a while. Think of it as a burned out kitty who doesn’t not want another petting session.

For the purpose of the tutorial, to not make it so long, I won’t create any kind of authentication system; however it’s entirely possible to do so and to fully test it client side with Cypress (you can intercept http requests and mock data). I would implement that first if it was necessary.

We’ll now start with e2e testing, describing the whole of what the game/application must be.

button.feature [located @ src/tests/e2e/specs]

Feature: The play button
The game will consist of a single button. The objective of the game is
to click it as much as possible without over clicking it too much.
Over clicking (pressing it more than 2 times in a second) leads us timed out
and unable to get more points (the button will be disabled until the countdown
finished).

Scenario: I press the button once when its available
# it should increase the game score
# and temporarily increase the panic score
When I click on the button
Then The game score should increase by 10

Scenario: I press the button 5 times in a second
# the panic score should be 100 and stay there
# until the countdown reaches 0
When I click on the button 5 times fast
Then The button should be disabled
And The button should be available after 5 seconds

Scenario: I press the button multiple times but slowly
# since you're pressing slowly enough you may click
When I click on the button 3 times slowly
Then The button should be available

button.js [located @ src/tests/e2e/steps]

// eslint-disable-next-line no-unused-vars
const { Given, When, Then } = require("cypress-cucumber-preprocessor/steps");

function click_button(n = 1, fast = true) {
// eslint-disable-next-line for-direction
cy.get("button#press_me").as("play_button");
if (fast) {
for (let i = 0; i < n; i++) {
cy.get("@play_button").click();
}
} else {
for (let i = 0; i < n; i++) {
cy.wait(2001).get("@play_button").click();
}
}
}
before(function () {
cy.visit("/");
});

When("I click on the button", function () {
click_button();
});

When("I click on the button {int} times slowly", function (n) {
click_button(n, false);
});

When("I click on the button {int} times fast", function (n) {
click_button(n);
});

When("I wait {int} seconds", function (n) {
cy.wait(n * 1000);
});

let curr_game_score = 0;

Then("The game score should increase by {int}", function (n) {
cy.get("span#game_score").contains(curr_game_score + n);
curr_game_score += n;
});

Then(/^The button should be available$/, function () {
cy.get("button#press_me").should("not.be.disabled");
});

Then(/^The button should be disabled$/, function () {
// press the button multiple times fast
cy.get("button#press_me").should("be.disabled");
});

Then("The button should be available after {int} seconds", function (n) {
cy.wait(n * 1000)
.get("button#press_me")
.should("not.be.disabled");
});

Step 4.1

Now, the unit testing, we’ll do one aspect which is testing the Vuex components of the game.

game.store.spec.js

import { game } from "@/store/game";
import { createStore } from "vuex";
// Mutations are simple, test them all at once
describe("testing mutations", () => {
it("addGameScore: should increase the stored game score value", () => {
const state = {
gameScore: 100,
};
game
.mutations.addGameScore(state, 10);
expect
(state.gameScore).toBe(110);
});
it
("addGamePanic: should increase the stored game panic value", () => {
const state = {
gamePanic: 100,
};
game
.mutations.addGamePanic(state, 10);
expect
(state.gamePanic).toBe(110);
});
it
("addGameTimeout: should increase the stored game timeout value", () => {
const state = {
gameTimeout: 0,
};
game
.mutations.addGameTimeout(state, 50);
expect
(state.gameTimeout).toBe(50);
});
it
("setGamePanicked: should set gamePanic to 100 and gameTimeout to 50", () => {
const state = {
gamePanic: 0,
gameTimeout: 0,
};
game
.mutations.setGamePanicked(state);
expect
(state.gamePanic).toBe(100);
expect
(state.gameTimeout).toBe(50);
});
});
describe("testing getters", () => {
it("isPanicked: true if store's game timeout is not 0", () => {
const state_panicked = {
gameTimeout: 1,
};
const state_cool = {
gameTimeout: 0,
};
expect
(game.getters.isPanicked(state_panicked)).toBe(true);
expect
(game.getters.isPanicked(state_cool)).toBe(false);
});
});
const createTestingStore = (state) => {
return createStore({
state: state,
mutations: game.mutations,
actions: game.actions,
});
};
describe("testing action press", () => {
it("should do nothing if game is 'panicked'", async () => {
// game is panicked if panic level if getter isPanicked is true
const state_initial = {
gameTimeout: 1, // panicked
gamePanic: 100,
gameScore: 42,
};
const store = createTestingStore(state_initial);
await store.dispatch("press");
expect
(state_initial).toStrictEqual(store.state);
});
it
("should set the game to panicked if the game isn't already but gamePanic >= 100", async () => {
const store = createTestingStore({
gameTimeout: 0,
gamePanic: 100,
gameScore: 42,
});
await store.dispatch("press");
expect
(store.state.gamePanic).toBe(100);
expect
(store.state.gameTimeout).toBe(50);
expect
(store.state.gameScore).toBe(52);
});
it
("should increase gameScore by 10 and gamePanic by 20 if its not panicked and gamePanic < 100", async () => {
const store = createTestingStore({
gameTimeout: 0,
gamePanic: 20,
gameScore: 50,
});
await store.dispatch("press");
expect
(store.state.gameScore).toBe(60);
expect
(store.state.gamePanic).toBe(40);
});
});
describe("testing action tick", () => {
it("should lower gamePanic by 1 if game isn't panicked", async () => {
const store = createTestingStore({
gameTimeout: 0,
gamePanic: 100,
gameScore: 50,
});
await store.dispatch("tick");
expect
(store.state.gameTimeout).toBe(0);
expect
(store.state.gamePanic).toBe(99);
expect
(store.state.gameScore).toBe(50);
});
it
("should lower gameTimeout by 1 if game is panicked", async () => {
const store = createTestingStore({
gameTimeout: 50,
gamePanic: 100,
gameScore: 50,
});
await store.dispatch("tick");
expect
(store.state.gameTimeout).toBe(49);
expect
(store.state.gamePanic).toBe(100);
expect
(store.state.gameScore).toBe(50);
});
});

Step 4

Go ahead and actually start to write your project’s codebase. Your objective is to make the tests you’ve written beforehand delightfully green. You’ll be running the tests quite a bit of time.

This step is skipped forward as it isn’t in what our scope of what our tutorial is about.

But first, you’ll want to make the unit tests happy.

hooray

Then, you’ll have to make the e2e tests happy too.

To completely view the project’s codebase, check out @ https://github.com/YiumPotato/attentionbutton.

Step 5

Step back and relax. Admire what you’ve done and start thinking of new features. Once freshened up, start by describing these new features using Cucumber; start repeating step 3 and 4 feature by feature if you’re going solo. You may want to learn more about the different kinds of testing and implore the depthful documentation available to further gather your skills at creating tests. Read about Agile development and how these theoretical skills are applied to learn the ways teams should work, and well how tech giants craft their software.

And most importantly, be proud of yourself, and keep going!

Thank you for reading.

--

--