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
- Specify a feature
- Connect the feature to Java
- Invoke the system under test
- Verify the wanted behaviour
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.
RunCukesTest.java- the glue that connects the features to JavaStepdefs.java- a connection to the system under testjunit-platform.properties- configuration for the Cucumber engine, like output formattingbelly.feature- the behaviour you want the system to be able to perform
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 file
- Connect the steps to Java
- Delegate to a helper class to actually run the system. This may seem as overkill at the moment, but it is a good idea later
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
- There isn't any user interface yet
- Steps performed in a user interface aren't the behaviour, they are the actions you perform to get a wanted behaviour
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
- Cucumber - the tool used in this tutorial
- Cucumber Expressions - the expression language used for step definitions
- Javalin - the web framework used in this tutorial
- Thomas Sundberg - your trainer