Increase your testing efficiency by utilizing Cucumber for Java application testing, fully integrated with Behaviour-Driven Development (BDD). This guide provides comprehensive steps for project setup, scenario writing, step implementation, and reporting.

1. Introduction

Cucumber is a tool that supports Behaviour-Driven Development (BDD). A good starting point in order to learn more about BDD and Cucumber, are the Cucumber guides. BDD itself has been introduced by Dan North in 2006, you can read his blog introducing BDD. Cucumber, however, is a tool that supports BDD, this does not mean you are practicing BDD just by using Cucumber. The Cucumber myths is an interesting read in this regard.

In the remainder of this blog, you will learn more about the features of Cucumber when developing a Java application. Do know, that Cucumber is not limited to testing Java applications, a wide list of languages is supported.

The sources used in this blog, can be found at GitHub.

2. Prerequisites

Prerequisites for this blog are:

  • Basis Java knowledge, Java 21 is used;
  • Basic Maven knowledge;
  • Basic comprehension of BDD, see the resources in the introduction.

3. Project Setup

An initial project can be setup by means of the Maven cucumber-archetype. Change the groupId, artifactId and package to fit your preferences and execute the following command:

$ mvn archetype:generate                      \
   "-DarchetypeGroupId=io.cucumber"           \
   "-DarchetypeArtifactId=cucumber-archetype" \
   "-DarchetypeVersion=7.17.0"               \
   "-DgroupId=mycucumberplanet"               \
   "-DartifactId=mycucumberplanet"               \
   "-Dpackage=com.mydeveloperplanet.mycucumberplanet"  \
   "-Dversion=1.0.0-SNAPSHOT"                 \
   "-DinteractiveMode=false"

The necessary dependencies are downloaded and the project structure is created. The output ends with the following:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.226 s
[INFO] Finished at: 2024-04-28T10:25:16+02:00
[INFO] ------------------------------------------------------------------------

Open the project with your favourite IDE. If you are using IntelliJ, a message is shown in order to install a plugin.

Take a closer look at the pom:

  • The dependencyManagement section contains BOMs (Bill of Materials) for Cucumber and JUnit;
  • Several dependencies are added for Cucumber and JUnit;
  • The build section contains the compiler plugin and the surefire plugin. The compiler is set to Java 1.8, change it into 21.
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-bom</artifactId>
            <version>7.17.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.10.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-junit-platform-engine</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <encoding>UTF-8</encoding>
                <source>21</source>
                <target>21</target>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
        </plugin>
    </plugins>
</build>

In the test directory, you will see a RunCucumberTest, StepDefinitions and an example.feature file in the resources section.

The RunCucumberTest file is necessary to run the feature files and the corresponding steps. The feature files and steps will be discussed later on, do not worry too much about it now.

@Suite
@IncludeEngines("cucumber")
@SelectPackages("com.mydeveloperplanet.mycucumberplanet")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
public class RunCucumberTest {
}

Run the tests, the output should be successful.

$ mvn test

4. Write Scenario

When practicing BDD, you will need to write a scenario first. Taken from the Cucumber documentation:

When we do Behaviour-Driven Development with Cucumber we use concrete examples to specify what we want the software to do. Scenarios are written before production code. They start their life as an executable specification. As the production code emerges, scenarios take on a role as living documentation and automated tests.

The application you need to build for this blog, is a quite basic one:

  • You need to be able to add an employee;
  • You need to retrieve the complete list of employees;
  • You need to be able to remove all employees.

A feature file follows the Given-When-Then (GWT) notation. A feature file consists out of:

  • A feature name, it is advised to maintain the same name as the file name;
  • A feature description;
  • One or more scenarios containing steps in the GWT notation. A scenario illustrates how the application should behave.
Feature: Employee Actions
  Actions to be made for an employee

  Scenario: Add employee
    Given an empty employee list
    When an employee is added
    Then the employee is added to the employee list

Run the tests and you will notice now that the feature file is executed. The tests fail of course, but example code is provided in order to create the step definitions.

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mydeveloperplanet.mycucumberplanet.RunCucumberTest

Scenario: Add employee                            # com/mydeveloperplanet/mycucumberplanet/employee_actions.feature:4
  Given an empty employee list
  When an employee is added
  Then the employee is added to the employee list
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.104 s <<< FAILURE! -- in com.mydeveloperplanet.mycucumberplanet.RunCucumberTest
[ERROR] Add an employee.Add employee -- Time elapsed: 0.048 s <<< ERROR!
io.cucumber.junit.platform.engine.UndefinedStepException: 
The step 'an empty employee list' and 2 other step(s) are undefined.
You can implement these steps using the snippet(s) below:

@Given("an empty employee list")
public void an_empty_employee_list() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}
@When("an employee is added")
public void an_employee_is_added() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}
@Then("the employee is added to the employee list")
public void the_employee_is_added_to_the_employee_list() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}

        at io.cucumber.core.runtime.TestCaseResultObserver.assertTestCasePassed(TestCaseResultObserver.java:69)
        at io.cucumber.junit.platform.engine.TestCaseResultObserver.assertTestCasePassed(TestCaseResultObserver.java:22)
        at io.cucumber.junit.platform.engine.CucumberEngineExecutionContext.lambda$runTestCase$4(CucumberEngineExecutionContext.java:114)
        at io.cucumber.core.runtime.CucumberExecutionContext.lambda$runTestCase$5(CucumberExecutionContext.java:136)
        at io.cucumber.core.runtime.RethrowingThrowableCollector.executeAndThrow(RethrowingThrowableCollector.java:23)
        at io.cucumber.core.runtime.CucumberExecutionContext.runTestCase(CucumberExecutionContext.java:136)
        at io.cucumber.junit.platform.engine.CucumberEngineExecutionContext.runTestCase(CucumberEngineExecutionContext.java:109)
        at io.cucumber.junit.platform.engine.NodeDescriptor$PickleDescriptor.execute(NodeDescriptor.java:168)
        at io.cucumber.junit.platform.engine.NodeDescriptor$PickleDescriptor.execute(NodeDescriptor.java:90)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

[INFO] 
[INFO] Results:
[INFO] 
[ERROR] Errors: 
[ERROR]   The step 'an empty employee list' and 2 other step(s) are undefined.
You can implement these steps using the snippet(s) below:

@Given("an empty employee list")
public void an_empty_employee_list() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}
@When("an employee is added")
public void an_employee_is_added() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}
@Then("the employee is added to the employee list")
public void the_employee_is_added_to_the_employee_list() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}

[INFO] 
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0

5. Add Step Definitions

Add the example code from the output above into the StepDefinitions file. Run the tests again. Of course they fail, but this time a PendingException is thrown indicating that the steps need to be implemented.

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mydeveloperplanet.mycucumberplanet.RunCucumberTest

Scenario: Add employee                            # com/mydeveloperplanet/mycucumberplanet/employee_actions.feature:4
  Given an empty employee list                    # com.mydeveloperplanet.mycucumberplanet.StepDefinitions.an_empty_employee_list()
      io.cucumber.java.PendingException: TODO: implement me
        at com.mydeveloperplanet.mycucumberplanet.StepDefinitions.an_empty_employee_list(StepDefinitions.java:12)
        at ✽.an empty employee list(classpath:com/mydeveloperplanet/mycucumberplanet/employee_actions.feature:5)

  When an employee is added                       # com.mydeveloperplanet.mycucumberplanet.StepDefinitions.an_employee_is_added()
  Then the employee is added to the employee list # com.mydeveloperplanet.mycucumberplanet.StepDefinitions.the_employee_is_added_to_the_employee_list()
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.085 s <<< FAILURE! -- in com.mydeveloperplanet.mycucumberplanet.RunCucumberTest
[ERROR] Add an employee.Add employee -- Time elapsed: 0.032 s <<< ERROR!
io.cucumber.java.PendingException: TODO: implement me
        at com.mydeveloperplanet.mycucumberplanet.StepDefinitions.an_empty_employee_list(StepDefinitions.java:12)
        at ✽.an empty employee list(classpath:com/mydeveloperplanet/mycucumberplanet/employee_actions.feature:5)

[INFO] 
[INFO] Results:
[INFO] 
[ERROR] Errors: 
[ERROR]   TODO: implement me
[INFO] 
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0

6. Implement Application

The first scenario is defined, let’s implement the application. Create a basic EmployeeService which adds the needed functionality. An employee can be added to an employees list which is just a map of employees. The list of employees can be retrieved and the list can be cleared.

public class EmployeeService {

    private final HashMap<Long, Employee> employees = new HashMap<>();
    private Long index = 0L;

    public void addEmployee(String firstName, String lastName) {
        Employee employee = new Employee(firstName, lastName);
        employees.put(index, employee);
        index++;
    }

    public Collection<Employee> getEmployees() {
        return employees.values();
    }

    public void removeEmployees() {
        employees.clear();
    }
}

The employee is a basic record.

public record Employee(String firstName, String lastName) {
}

7. Implement Step Definitions

Now that the service exists, you can implement the step definitions. It is rather straightforward, you create the service and invoke the methods for the Given-When implementations. Verifying the result is done by Assertions, just as you would do for your unit tests.

public class StepDefinitions {

    private final EmployeeService service = new EmployeeService();

    @Given("an empty employee list")
    public void an_empty_employee_list() {
        service.removeEmployees();
    }
    @When("an employee is added")
    public void an_employee_is_added() {
        service.addEmployee("John", "Doe");
    }
    @Then("the employee is added to the employee list")
    public void the_employee_is_added_to_the_employee_list() {
        assertEquals(1, service.getEmployees().size());
    }

}

Run the tests, which are successful now.

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mydeveloperplanet.mycucumberplanet.RunCucumberTest

Scenario: Add employee                            # com/mydeveloperplanet/mycucumberplanet/employee_actions.feature:4
  Given an empty employee list                    # com.mydeveloperplanet.mycucumberplanet.StepDefinitions.an_empty_employee_list()
  When an employee is added                       # com.mydeveloperplanet.mycucumberplanet.StepDefinitions.an_employee_is_added()
  Then the employee is added to the employee list # com.mydeveloperplanet.mycucumberplanet.StepDefinitions.the_employee_is_added_to_the_employee_list()
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.081 s -- in com.mydeveloperplanet.mycucumberplanet.RunCucumberTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

8. Extra Scenario

Add a second scenario which tests the removal of employees. Add the scenario to the feature file.

Scenario: Remove employees
    Given a filled employee list
    When the employees list is removed
    Then the employee list is empty

Implement the step definitions.

@Given("a filled employee list")
public void a_filled_employee_list() {
    service.addEmployee("John", "Doe");
    service.addEmployee("Miles", "Davis");
    assertEquals(2, service.getEmployees().size());
}
@When("the employees list is removed")
public void the_employees_list_is_removed() {
    service.removeEmployees();
}
@Then("the employee list is empty")
public void the_employee_list_is_empty() {
    assertEquals(0, service.getEmployees().size());
}

9. Tags

In order to run a subset of scenarios, you can add tags to features and scenarios.

@regression
Feature: Employee Actions
  Actions to be made for an employee

  @TC_01
  Scenario: Add employee
    Given an empty employee list
    When an employee is added
    Then the employee is added to the employee list

  @TC_02
  Scenario: Remove employees
    Given a filled employee list
    When the employees list is removed
    Then the employee list is empty

Run only the test annotated with TC_01 by using a filter.

$ mvn clean test -Dcucumber.filter.tags="@TC_01"
...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.mydeveloperplanet.mycucumberplanet.RunCucumberTest
[WARNING] Tests run: 2, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.233 s -- in com.mydeveloperplanet.mycucumberplanet.RunCucumberTest
[INFO] 
[INFO] Results:
[INFO] 
[WARNING] Tests run: 2, Failures: 0, Errors: 0, Skipped: 1

10. Reporting

When executing tests, it is often required that appropriate reporting is available. Up till now, only console output has been shown.

Generate an html report by adding the following configuration parameter to the RunCucumberTest.

@Suite
@IncludeEngines("cucumber")
@SelectPackages("com.mydeveloperplanet.mycucumberplanet")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "html:target/cucumber-reports.html")
public class RunCucumberTest {
}

After running the test, a rather basic html report is available in the specified path.

Several third-party reporting plugins are available. The cucumber-reporting-plugin offers a more elaborate report. Add the dependency to the pom.

<dependency>
    <groupId>me.jvt.cucumber</groupId>
    <artifactId>reporting-plugin</artifactId>
    <version>5.3.0</version>
</dependency>

Enable the report in RunCucumberTest.

@Suite
@IncludeEngines("cucumber")
@SelectPackages("com.mydeveloperplanet.mycucumberplanet")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "html:target/cucumber-reports.html")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "me.jvt.cucumber.report.PrettyReports:target/cucumber")
public class RunCucumberTest {
}

Run the tests and in the target/cucumber directory the report is generated. Open the file starting with report-feature.

11. Conclusion

Cucumber has great support for BDD. It is quite easy to use and in this blog, you only scratched the surface of its capabilities. An advantage is that you can make use of JUnit and Assertions and the steps can be implemented by means of Java. No need to learn a new language when your application is also built in Java.