Cucumber-JVM

This is the instructions for a Cucumber-JVM tutorial. That is, you will learn to use the Java version of the tool Cucumber.

Content

Part 1 - Introduction

Cucumber is a tool for automation steps defined in Gherkin and is a part of Behaviour Driven Development, BDD.

You specify a wanted behaviour in a human readable form. This human readable specification can then be automated. You don't have to automate your wanted behaviour, but it will give advantages if you do.

This tutorial will show you how the implementation can be done.

How does it work?

A 30 second description is

Specifying features

The wanted behaviour is defined in files with the suffix .feature. A feature consist of one or many scenarios. A scenario is an example expressed using the keywords Given, When, Then, And or But

An example of a feature file is

Feature: Belly

  Scenario: a few cukes
    Given I have 42 cukes in my belly
    When I wait 1 hour
    Then my belly should growl

This example describes how your tummy could behave when you have eaten many cucumbers. These short sentences are used to drive the implementation and verify that the behaviour you want is the behaviour you experience.

Glue the features to Java

The feature defined above needs to be connected to the programming language you use. In this case, you will use Java. The glue is defined using a JUnit 5 test suite configured to use the Cucumber engine. The runner class tells JUnit to use Cucumber and where to find the .feature files. An example may look like this:

package skeleton;

import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("skeleton")
public class RunCukesTest {
}

Configuration for the Cucumber engine, such as output formatting, is handled separately in a file called junit-platform.properties located under src/test/resources/.

Connect to the system under test

The last part is to connect the steps defined in the feature with the system you are about to implement. It is done using Java methods annotated with Cucumber Expressions. Cucumber Expressions are intuitive and there is no need to worry about them. An example of an annotated method is this one:

@Given("I have {int} cukes in my belly")
public void i_have_cukes_in_my_belly(int cukes) {
    Belly belly = new Belly();
    belly.eat(cukes);
}

The annotation defines a Cucumber Expression. The {int} is a parameter type that matches an integer and automatically converts it to the int cukes parameter. Parameter types like these are our way to get hold of the values you want to read from the example.

Verify the behaviour

The verification is done in the last step. In the case above, you would try to hear a growling sound and verify that the sound returned from our system is a growl.

The verification will be done using an assert. Cucumber don't define any asserts, use an assert from your test framework. In our case, JUnit 5. I often use assertEquals() because it gives me nice feedback when something is broken.

A vending machine

You will build a vending machine. A vending machine that will make you coffee, tea or chocolate. You will need to tell it what you want. It is a generous system, you should ignore things like charging for the beverage. Unfortunately, you also have to ignore the hardware part and not prepare any beverage.
But it will have a user interface that accepts a selection and acknowledges your order!

Setup

Before you get started, there are a few setup steps that must be handled. You need a modern Java and a skeleton example to start from. Later, Firefox will be used for testing. Selenium will download it automatically if it is missing.

Verify that you can compile Java 25 code

Open a command prompt or terminal. Enter

javac -version

The wanted response is something similar to

javac 25

The last digits are probably different.

Get the getting started project

There is a getting started project called cucumber-java-skeleton that you need to clone or download. It contains enough infrastructure for you to get started. It is as small as it gets and includes build scripts for both Maven and Gradle.

Clone the getting started project

If you have access to Git one way of getting a copy of the skeleton project is to clone it.

git clone https://github.com/tsundberg/cucumber-java-skeleton.git

Download a zip

Download the project as a zip file from https://github.com/tsundberg/cucumber-java-skeleton/archive/master.zip. Unpack it and use it as a starting point.

File structure

The most important files in the skeleton you downloaded are these:

cucumber-java-skeleton
|-- .mvn
|-- build.gradle
|-- gradle
|-- gradlew
|-- gradlew.bat
|-- mvnw
|-- mvnw.cmd
|-- pom.xml
`-- src
    |-- main
    |   `-- java
    |       `-- skeleton
    |           `-- Belly.java
    `-- test
        |-- java
        |   `-- skeleton
        |       |-- RunCukesTest.java
        |       `-- Stepdefs.java
        `-- resources
            |-- junit-platform.properties
            `-- skeleton
                `-- belly.feature

There are build scripts for Maven, pom.xml, and Gradle, build.gradle. There are also wrappers for both, mvnw and gradlew. These are important for building our example.

There are also four files that actually are a part of the Cucumber setup.

You will create files like those above and implement the wanted behaviour for our vending machine.

Finally, there is a production class called Belly that implement some functionality in this skeleton. It is rather uninteresting in all aspects but the aspect of seeing that your are able to run production code from Cucumber.

Run a build

Next step is to actually execute Cucumber. Run the build from the root of the project.

Maven

./mvnw test

On Windows, use mvnw test instead.

Gradle

./gradlew test

On Windows, use gradlew test instead.

The wrappers will download a working copy of the build tool automatically. This is the zero setup alternative, you don't have to install any build tool, all you need is bundled in the skeleton project.

Implement missing steps

The downloaded skeleton is missing a few steps. They are reported as missing at the moment. I saw this when I executed the build:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running skeleton.RunCukesTest

Scenario: a few cukes                 # classpath:skeleton/belly.feature:3
  ✔ Given I have 42 cukes in my belly # skeleton.Stepdefs.i_have_cukes_in_my_belly(int)
  ■ When I wait 1 hour
  ↷ Then my belly should growl

The step 'I wait 1 hour' and 1 other step(s) are undefined.
You can implement these steps using the snippet(s) below:

@When("I wait {int} hour")
public void i_wait_hour(Integer int1) {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}

@Then("my belly should growl")
public void my_belly_should_growl() {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}

Tests run: 1, Failures: 0, Errors: 1, Skipped: 0

Implement the missing steps in src/test/java/skeleton/Stepdefs.java using the suggestion from the execution.

You can continue to implement the production code for the belly.feature, that is add the functionality needed in src/main/java/skeleton/Belly.java if you want. The rest of this tutorial is not depending on it. Feel free to skip it for now.

Part 2 - Implementing a vending machine

You have just been introduced to Cucumber. It is now time to implement a complete application with a user interface and a backing model.

Specify the model

The wanted behaviour should be specified using Gherkin and the format Given/When/Then. This specification will be used to drive the actual implementation. The specification will be done in these steps:

Create a new Feature

Create a new file called Order.feature in src/test/resources/skeleton

The feature file must be placed in the skeleton directory because @SelectClasspathResource("skeleton") in RunCukesTest tells Cucumber to look for .feature files in that directory on the classpath. The directory src/test/resources/skeleton/ maps to skeleton on the classpath.

The content of this new feature file should be

src/test/resources/skeleton/Order.feature

Feature: Brew coffee

  In order to get the refreshment of coffee,
  as a developer,
  I want to order a fresh brewed coffee from a coffee machine.

  Scenario: Order a cup of black coffee
    Given I am at the vending machine
    When I order one cup of coffee
    Then coffee is served

A feature file must start with the keyword Feature followed by a headline describing this feature.

There may be any text before the next keyword Scenario. The space between Feature and Scenario is available for anything you find useful. It can be a user story, it can be rules you discover during you example mapping session. Anything that will help you understand the feature better.

The scenario is then described. Preferably with the behaviour you want, not the steps you should perform to get the wanted behaviour.

In this example, I don't mention anything about the user interface. There are two reasons for this

Remove belly.feature

The file src/test/resources/skeleton/belly.feature will cause us a problem later if you keep it. Delete it from the project.

Also delete src/test/java/skeleton/Stepdefs.java and src/main/java/skeleton/Belly.java since they belong to the belly example. After deleting these files, run a clean build (./mvnw clean test or ./gradlew clean test) so that stale copies in the build output are removed.

Implement the missing methods

With a feature and a scenario defined, you need to connect to Java. This is done by implementing methods that are annotated so they match the steps defined in the feature file you created above. A simple way to get started is to run the build and copy the snippets to the file src/test/java/skeleton/VendingSteps.java.

My implementation looks like:

src/test/java/skeleton/VendingSteps.java

package skeleton;

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

public class VendingSteps {
    @Given("I am at the vending machine")
    public void i_am_at_the_vending_machine() {
        // Write code here that turns the phrase above into concrete actions
        throw new io.cucumber.java.PendingException();
    }

    @When("I order one cup of coffee")
    public void i_order_one_cup_of_coffee() {
        // Write code here that turns the phrase above into concrete actions
        throw new io.cucumber.java.PendingException();
    }

    @Then("coffee is served")
    public void coffee_is_served() {
        // Write code here that turns the phrase above into concrete actions
        throw new io.cucumber.java.PendingException();
    }
}

Get the parameters

The parameters extracted from the example are located using Cucumber Expressions. Cucumber Expressions use parameter types enclosed in curly braces to match and convert values.

In the example above, the one with the belly, you saw {int}. That is a parameter type matching any integer.

To match a single word, you can use {word}. It will match a single word without whitespace and is great for catching the word coffee in this example. To match a quoted string, use {string}.

More about the expressions used in Cucumber can be found in the Cucumber Expressions documentation.

Any class

The methods Cucumber searches for, the ones annotated, can be defined in any class in the same package as the runner class or in a subpackage. Cucumber scans the package matching the @SelectClasspathResource value and finds all annotated step definitions automatically. This gives you the freedom to collect all steps that belong together in one class. Cucumber will pick the unique methods from its view of the world.

Global methods may feel strange and ugly, but it works in this context. Each step has to be unique, each annotation will therefore have to be unique. So global yes, but there will only be one Cucumber Expression that matches. The reasoning behind this decision is that if you describe two different parts of your system using the same words, you have a much bigger problem than global methods and ambiguous expressions.

A delegation class

With the feature connected to Java you are able to start working with the actual system. It is, however, a good idea to separate things sometimes. In this case, separate connecting features to Java from executing the system. These are two different things and they change for different reasons.

The way this separation is done is by creating a helper class that contains the logic to connect to the system you will build.

Create a new file called

src/test/java/skeleton/VendingHelper.java

Creating the delegation class now will make it defined when you use it below.

Use the step definitions to create the methods you would like to delegate to. The goal is to have one or two, not more, lines in each step.

Program in a wishful way, do not consider what you have. Instead, create the API you wish you had.

The step definitions may look like this:

src/test/java/skeleton/VendingSteps.java

package skeleton;

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 VendingSteps {
    private VendingHelper vendingHelper;

    @Given("I am at the vending machine")
    public void i_am_at_the_vending_machine() {
        vendingHelper = new VendingHelper();
    }

    @When("I order one cup of {word}")
    public void i_order_one_cup_of(String beverage) {
        vendingHelper.orderBeverage(beverage);
    }

    @Then("{word} is served")
    public void is_served(String expected) {
        String actual = vendingHelper.getBeverage();
        assertEquals(expected, actual);
    }
}

This drives the helper class to be implemented like this:

src/test/java/skeleton/VendingHelper.java

package skeleton;

public class VendingHelper {
    private VendingMachine vendingMachine;

    public VendingHelper() {
        vendingMachine = new VendingMachine();
    }

    public void orderBeverage(String beverage) {
        vendingMachine.order(beverage);
    }

    public String getBeverage() {
        return vendingMachine.getOrder();
    }
}

The helper class and the calls to the model have been created before the model. This allowed me to once again create the API I would love to have for this problem.

Implement the model

Finally, let us implement the model supporting our vending machine. The API has been defined in the previous step. Now you have to make sure that the behaviour defined in the scenario above works as intended.

I implemented the model like this:

src/main/java/skeleton/VendingMachine.java

package skeleton;

public class VendingMachine {
    private String beverage;

    public void order(String beverage) {
        this.beverage = beverage;
    }

    public String getOrder() {
        return beverage;
    }
}

Build the application using ./mvnw test or ./gradlew test.

There are some obvious things you might want to fix here. The type for beverage should probably not be a String. But let us ignore that today. This is after all just an example where you will get a feeling for how you can implement the automation possible when you use BDD with Cucumber.

Add a web application

Next step is to put a web application in front of the model. Before you can start building it, some additional infrastructure is needed.

An executable jar

The first thing you will do is to create an executable jar. When it works, you will add dependencies for a web application.

Maven

Add the maven-shade-plugin to the plugins section in pom.xml:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.6.1</version>
    <configuration>
        <createDependencyReducedPom>true</createDependencyReducedPom>
        <filters>
            <filter>
                <artifact>*:*</artifact>
                <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                </excludes>
            </filter>
        </filters>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>skeleton.Main</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

Gradle

Add the com.gradleup.shadow plugin to build.gradle:

plugins {
    id 'java'
    id 'com.gradleup.shadow' version '9.3.1'
}

Also add a jar block to configure the main class:

jar {
    manifest {
        attributes 'Main-Class': 'skeleton.Main'
    }
}

Build a Hello World application

Time to verify our executable jar. Create a class called Main in src/main/java/skeleton and add a main(String[] args) method to it. The result should look like this:

src/main/java/skeleton/Main.java

package skeleton;

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

This is a pretty useless implementation, but it will be enough to verify that you are able to create an executable jar. Build the application and run it.

Maven

Build

./mvnw clean install

Run the application

java -jar target/cucumber-java-skeleton-0.0.1.jar

Gradle

Build

./gradlew clean shadowJar

Run the application

java -jar build/libs/cucumber-java-skeleton-all.jar

Verify that you can see "Hello, World!" in your console.

Add dependencies for a web application

With an executable jar, it is now time to build a web application and verify it on localhost.

Start with adding a few dependencies. You will use

Maven

Add these dependencies in your pom.xml:

<dependency>
    <groupId>io.javalin</groupId>
    <artifactId>javalin</artifactId>
    <version>6.7.0</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>2.0.17</version>
</dependency>

<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.41.0</version>
    <scope>test</scope>
</dependency>

Gradle

Add these dependencies in your build.gradle:

implementation 'io.javalin:javalin:6.7.0'
implementation 'org.slf4j:slf4j-simple:2.0.17'
testImplementation 'org.seleniumhq.selenium:selenium-java:4.41.0'

You may need to re-import the project in your IDE.

A web application

This web application will say hi. It is enough to verify that you haven't made any mistakes and that you are able to implement a more complicated web application.

Start by creating an HTML file that says hello. Create the file src/main/resources/hello.html:

src/main/resources/hello.html

<!DOCTYPE HTML>
<html>
<p>Hello, world!</p>
</html>

Define a route in main and return the page. The HTML is loaded from the classpath at runtime using getResourceAsStream.

Update your Main class to look like this:

src/main/java/skeleton/Main.java

package skeleton;

import io.javalin.Javalin;

import java.io.InputStream;

public class Main {
    private static Javalin app;

    public static void main(String[] args) {
        app = Javalin.create().start(7070);

        app.get("/", ctx -> {
            InputStream stream = Main.class.getResourceAsStream("/hello.html");
            byte[] bytes = stream.readAllBytes();
            String html = new String(bytes);
            ctx.html(html);
        });
    }

    public static void shutdown() {
        app.stop();
    }
}

This means that our application will listen on / and return the HTML when someone visit.

Build and run the application as you did above. Verify that you can see a "Hello, world!" page on http://localhost:7070.

That was the infrastructure needed for our web application. Let us now continue with a our real goal, a vending machine that will give you coffee.

A user interface

The infrastructure for a small web application is in place and verified. Our next step is to add the first user interface to our vending machine. It will need one page where you can order the beverage you want and one page that confirm your order.

You want to allow the wanted behaviour to drive our implementation. This means that you should start with the tests needed and build just enough to satisfy what you currently know. Not more, not less.

Start with updating the helper class to use Selenium for communication with the application. An implementation may now look like this:

src/test/java/skeleton/VendingHelper.java

package skeleton;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.Select;

public class VendingHelper {
    private WebDriver browser;

    public VendingHelper() {
        String[] args = {};
        Main.main(args);

        browser = new FirefoxDriver();
    }

    public void orderBeverage(String beverage) {
        browser.get("http://localhost:7070");

        WebElement beverages = browser.findElement(By.name("beverages"));
        Select select = new Select(beverages);
        select.selectByValue(beverage);

        WebElement orderButton = browser.findElement(By.name("submit"));
        orderButton.click();
    }

    public String getBeverage() {
        WebElement selection = browser.findElement(By.id("confirmation"));
        String confirmationText = selection.getText();
        String[] parts = confirmationText.split(" ");
        return parts[1];
    }

    public void stop() {
        browser.quit();
        Main.shutdown();
    }
}

If you try to compile now, you will notice that Main.shutdown() doesn't exist yet. Don't worry, you will implement it soon enough.

The VendingHelper would benefit from using a Page Object. But it would mean too many details today.

Now you need two pages and routing between them. Start by creating the HTML files as external resources.

Create the order form page:

src/main/resources/order.html

<!DOCTYPE HTML>
<html>

<form id="conversion" method="post" action="order">
    <label for="beverage">Beverage:
        <select name="beverages">
            <option value="none"></option>
            <option value="coffee">Coffee</option>
            <option value="tea">Tea</option>
            <option value="chocolate">Chocolate</option>
        </select>
    </label>
    <br>
    <input name="submit" type="submit" value="Get your beverage">
</form>

</html>

Create the confirmation page. Note the %s placeholder that will be replaced with the actual order:

src/main/resources/confirmation.html

<!DOCTYPE HTML>
<html>
<p id="confirmation">One %s is prepared for you!</p>
</html>

Update your Main class to load both HTML files from the classpath. I have updated it to look like this:

src/main/java/skeleton/Main.java

package skeleton;

import io.javalin.Javalin;

import java.io.InputStream;

public class Main {
    private static Javalin app;
    private static VendingMachine vendingMachine = new VendingMachine();

    public static void main(String[] args) {
        app = Javalin.create().start(7070);

        app.get("/", ctx -> {
            InputStream stream = Main.class.getResourceAsStream("/order.html");
            byte[] bytes = stream.readAllBytes();
            String html = new String(bytes);
            ctx.html(html);
        });

        app.post("/order", ctx -> {
            String beverage = ctx.formParam("beverages");
            vendingMachine.order(beverage);

            // Assume that the hardware will do something with your order

            String order = vendingMachine.getOrder();
            InputStream stream = Main.class.getResourceAsStream("/confirmation.html");
            byte[] bytes = stream.readAllBytes();
            String template = new String(bytes);
            ctx.html(template.formatted(order));
        });
    }

    public static void shutdown() {
        app.stop();
    }
}

The routing is done in the main method. I have also added an instance of our VendingMachine. It hasn't changed, but it is our abstraction between us and the hardware that someone else will build.

The HTML for both pages is loaded from external files using getResourceAsStream. The confirmation page uses String.formatted() to insert the order into the HTML, replacing the %s placeholder.

I have also added a shutdown() method so the system can be controlled from a test. It is called from the VendingHelper.

The last thing to do before you are able to do a test run is to add a clean up step in the steps implementation. Extend your VendingSteps with this method:

src/test/java/skeleton/VendingSteps.java

@After
public void after() {
    vendingHelper.stop();
}

The import for @After is io.cucumber.java.After.

This method will be executed after each scenario and it will shut down the web application. If the web application isn't shut down, it would be left running and there would be issues with ports being used later.

Time to do a test run. Build the system with ./mvnw test or ./gradlew test and see Firefox use the application. Try and change the feature to use something you can't select and see that the execution will fail.

Conclusion

It is possible to create a model from a wanted behaviour and then use the same model when a user interface is added on top of it.

It is possible to build a lightweight web application using Java.

Interested in more? Ask Thomas for more. Drop him an e-mail at thomas@thinkcode.se

References