Write your first BDD tests using Cucumber

Elharem Soufiane
8 min readApr 17, 2024

--

Introduction

Hi 🤚 I’m Soufiane, a software engineer. In this article, we’re going to learn how to implement the BDD (Behavior-Driven Development) process using a tool called Cucumber in Java. So, for that, you’ll just need a working Java environment and an installed IDE.

Behavioral-Driven Development

Behavior-Driven Development (BDD) fosters collaboration among different stakeholders in software development, including developers and product owners. It achieves this by creating a shared understanding of the system’s behavior through the definition of common scenarios.

In a standard approach, the product owner typically provides specifications for a feature, often documented in a tool like Jira, and assigns the ticket to developers. The developers then analyze the requirements to gain a clear understanding of the feature before proceeding with implementation. Once the development is complete, testers review the code and perform testing to ensure that it meets the specified requirements. Then, the rest of the process (merge, deploy) follows.

In the BDD approach, however, they define together user stories or scenarios using a structured language, which captures the desired behavior of the system from the perspective of end users. These scenarios or behaviors serve as executable specifications that drive development, hence the term Behavioral-Driven Development. Developers then work iteratively to implement those scenarios. We start with a failing scenario, implement the minimum to make it pass, then refactor and create additional scenarios and features until development is complete. This allows for shorter feedback loops between the product owner, tester, and developers.

BDD process

Cucumber

Now, to implement BDD, we use tools like Cucumber. It enables us to implement BDD using a natural language syntax called Gherkin, which we will learn about in the following sections. There are other alternatives to Cucumber such as SpecFlow for .NET, Behave for Python, Gauge, etc. They all follow the same principle.

Getting started

We start by creating an empty Cucumber project. I am going to use IntelliJ IDEA in this blog, but the same steps apply to any other IDEs like Eclipse or VS Code. You can create a Cucumber project by running the following command in the terminal :

mvn archetype:generate                        \
"-DarchetypeGroupId=io.cucumber" \
"-DarchetypeArtifactId=cucumber-archetype" \
"-DarchetypeVersion=7.16.1" \
"-DgroupId=CucumberDemo" \
"-DartifactId=CucumberDemo" \
"-Dpackage=CucumberDemo" \
"-Dversion=1.0.0-SNAPSHOT" \
"-DinteractiveMode=false"

This command creates a new Maven project based on the Cucumber archetype.

You can also create an empty Java project and then add the necessary files and dependencies.

Once the project is generated, you’ll find an example of a Cucumber project that runs well, with the following structure:

projectName
├── src
│ │
│ └── test
│ ├── java
│ │ └── CucumberDemo
│ │ ├── RunCucumberTest.java
│ │ └── StepDefinitions.java
│ └── resources
│ └── CucumberDemo
│ └── example.feature
└── pom.xml

Delete the two files example.feature and StepDefinitions because we’re going to create our own files.

To start writing a BDD test, we need two things:

1. A feature file : This file is responsible for describing the feature we want to develop.
2. A StepDefinition file : It’s a class that’s responsible for the actual behavior of the feature.

We’re going to start by describing our feature. In our case, we’re trying to build a simple game of finding a lucky number. We generate a random number, and if that number is a lucky number (for our case, it’s the number 3), we’re lucky; otherwise, we’re unfortunate. It’s a simple game, isn’t it? Maybe it’s not that exciting, but you can build whatever you want, maybe a CS:GO game with that approach.

Creating a feature file

In the directory resources/(project name)/features, we create a feature file lucky-number with the extension .feature. We add it to the features folder because our application will consist of multiple features, not just one!

Our feature file will look like this :

Feature: Finding the lucky number

Scenario : Get lucky number
Given a random number is generated as 1
When I verify if the lucky number is 3
Then I expect to receive the answer"Unfortunate"

This file describes our feature using Gherkin syntax. It’s a language used by product teams to define requirements for developers to implement using Cucumber. We define our feature with the keyword ‘Feature’ followed by a brief description, such as ‘finding the lucky number.’ Next, we create scenarios using the keyword ‘Scenario.’ Within a single feature file, we can include multiple scenarios. In our case, we’ll start with a simple scenario: ‘Get lucky number with simple rules.’ In this scenario, we select the number 1, which doesn’t match the lucky number 3, resulting in the user receiving ‘Unfortunate’ as the outcome.

As we’ve implemented our first scenario, let’s test it. Run the following command in the terminal.

mvn test

Then, you’ll encounter an error:

[ERROR] Errors: 
[ERROR] The step 'a random number is generated as 1' and 2 other step(s) are undefined.
You can implement these steps using the snippet(s) below:

@Given("a random number is generated as 1")
public void a_random_number_is_generated_as_1(Integer int1) {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java.PendingException();
}
@When("I verify if the lucky number is 3")
public void i_verify_if_the_lucky_number_is_3() {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java.PendingException();
}
@Then("I expect to receive the answer {string}")
public void i_expect_to_receive_the_answer(String string) {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java.PendingException();
}

By default, Cucumber scans the entire classpath for step definitions. In large projects, this can be inefficient, so it’s better to configure the glue path. You’re essentially telling Cucumber to look for step definitions in specific packages or directories. Since we only use one feature file, there’s no need for it.

The error occurs because we haven’t defined our steps yet.

Creating step definition

Let’s define our steps now. Copy the methods thrown in the error and paste them into a newly created class

src/test/java/CucumberDemo/FindingTheLuckyNumber.java

package CucumberDemo;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

public class FindingTheLuckyNumber {
@Given("a random number is generated as 1")
public void a_random_number_is_generated_as_1(Integer int1) {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java.PendingException();
}

@When("I verify if the lucky number is 3")
public void i_verify_if_the_lucky_number_is_3() {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java.PendingException();
}

@Then("I expect to receive the answer {string}")
public void i_expect_to_receive_the_answer(String string) {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java.PendingException();
}
}

Run cucumber again, you’ll see that error :

1 Scenarios (1 pending)
3 Steps (2 skipped, 1 pending)

Implementing step definition

Now, tests are pending. You should implement the steps so that the tests can pass.

package CucumberDemo;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class FindingTheLuckyNumber {

private int randomNumber;
private String answer;
private static final int LUCKY_NUMBER = 3;

@Given("a random number is generated as 1")
public void a_random_number_is_generated_as_1() {
this.randomNumber = 1;
}

@When("I verify if the lucky number is 3")
public void i_verify_if_the_lucky_number_is_3() {
answer = null;
}

@Then("I expect to receive the answer {string}")
public void i_expect_to_receive_the_answer(String expectedAnswer) {
assertEquals(expectedAnswer, answer);
}
}

When you run Cucumber, you’ll have:

Failed scenarios:
src/test/resources/CucumberDemo/features/lucky_number.feature:3 # Finding the lucky number
1 Scenarios (1 failed)
3 Steps (1 failed, 2 passed)

So, one of the 3 steps failed. It’s the third one. You should follow the BDD process and write the minimal code to make it pass.

package CucumberDemo;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class FindingTheLuckyNumber {

private int randomNumber;
private String answer;
private static final int LUCKY_NUMBER = 3;

@Given("a random number is generated as 1")
public void a_random_number_is_generated_as_1() {
this.randomNumber= 1;
}

@When("I verify if the lucky number is 3")
public void i_verify_if_the_lucky_number_is_3() {
answer = "Unfortunate";
}

@Then("I expect to receive the answer {string}")
public void i_expect_to_receive_the_answer(String expectedAnswer) {
assertEquals(expectedAnswer, answer);
}
}

Run Cucumber again

1 Scenarios (1 passed)
3 Steps (3 passed)

All three of our steps have passed. Now, let’s take a moment to refactor our method names to align with our coding style.

We’ll rename them as follows: generateRandomNumber, askIfNumberIsLucky, and verifyLuckyNumberResponse.

Moving forward, we aim to enhance the versatility of our code. We’ll achieve this by generalizing the generated number and response by introducing two variables.

Adding variables

Feature: Finding the lucky number

Scenario : Get lucky number
Given a random number is generated <randomNumber>
When I verify if the generated number is the lucky number
Then I expect to receive the answer "<expectedAnswer>"

Additionally, we’ll incorporate examples to illustrate our changes effectively. That improves understanding of the feature and also covers outline cases, if any. To enable this, we need to modify Scenario to Scenario Outline :

Adding examples

Feature: Finding the lucky number

Scenario Outline : Get lucky number
Given a random number is generated <randomNumber>
When I verify if the generated number is the lucky number
Then I expect to receive the answer "<expectedAnswer>"

Examples:
| randomNumber | expectedAnswer |
| 1 | Unfortunate |
| 2 | Unfortunate |
| 3 | Lucky |

The steps are going to fail. This is normal. As previously, you should implement them so they can pass with the minimum code.

package CucumberDemo;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class FindingTheLuckyNumber {

private int randomNumber;
private String answer;
private static final int LUCKY_NUMBER = 3;

@Given("a random number is generated {int}")
public void generatingARandomNumber(int randomNumber) {
this.randomNumber = randomNumber;
}

@When("I check if the generated number is the lucky number")
public void iCheckWeitherIFindTheLuckyNumber() {
answer = (randomNumber == LUCKY_NUMBER) ? "Lucky" : "Unfortunate";
}

@Then("I should be told {string}")
public void iShouldBeTold(String expectedAnswer) {
assertEquals(expectedAnswer, answer);
}
}

Tests are going to pass now.

1 Scenarios (1 passed)
3 Steps (3 passed)

Conclusion

If you’ve reached this step, congratulations 🥳! You’ve just completed your first BDD test. Of course, if you’re serious about using Cucumber and BDD, you’ll be building more complex and useful applications instead of a simple program. So, you should create more scenarios and features, resulting in more step definitions. The same process applies :

write a scenario → write step definitions → fail the test → write the minimum code to pass the test → refactor → repeat.

If you want to learn more about using Cucumber, visit the official documentation at https://cucumber.io/docs.

You can find the source code of this app in the following repo :

https://github.com/Selharem/CucumberDemo

If you find this post useful, make sure to follow me on:

Thank you for reading. See you in another blog ✋.

--

--