Whether behaviour-driven-development, specification by example or acceptance test driven development is the goal, the Cucumber framework eases our life when we need to establish a link between the non-technical, textual description for a new feature and the tests that prove that the application fulfils these requirements.
In the following short tutorial I’d like to demonstrate how to add Cucumber to a Java project and how to write feature descriptions and test-cases for each step of these descriptions.
Dependencies
To run the following examples, we need the following three dependencies: junit, cucumber-java and cucumber-junit (using cucumber 1.2.0). Using Maven, adding the following snippet to our pom.xml should add everything we need here.
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
A simplified Workflow (Suggestion)
-
Get users, developers, testers, product-owners etc.. together
-
They describe the behaviour of a new feature in plain text and using the Gherkin syntax
-
Developers add the derived feature-files to the project and integrate the cucumber-junit-testrunner
-
Run the tests and watch them fail – cucumber prints snippets for the glue code that can be used for writing the step/glue-classes.
-
Write the code to make the first test (step) pass
-
Repeat until everything is green
Example 1: Book Search
Writing the Feature
This is our first feature: A customer shall be able to search books – stored in a file named search_book.feature. Note: Writing possible input parameters in single quotes make it easy for cucumber to generate the glue code for the step files with the correct parameters.
Feature: Book search
To allow a customer to find his favourite books quickly, the library must offer multiple ways to search for a book.
Scenario: Search books by publication year
Given a book with the title 'One good book', written by 'Anonymous', published in 14 March 2013
And another book with the title 'Some other book', written by 'Tim Tomson', published in 23 August 2014
And another book with the title 'How to cook a dino', written by 'Fred Flintstone', published in 01 January 2012
When the customer searches for books published between 2013 and 2014
Then 2 books should have been found
And Book 1 should have the title 'Some other book'
And Book 2 should have the title 'One good book'
Domain Objects
We have to simple classes here in our domain model: library and book.
Book
This is our simple book class: A book has a title, an author an a publication date.
package com.hascode.tutorial.cucumber.book;
import java.util.Date;
public class Book {
private final String title;
private final String author;
private final Date published;
// constructors, getter, setter ommitted
}
Library
This is our library class: Our library allows us to add books and find books.
package com.hascode.tutorial.cucumber.book;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
public class Library {
private final List<Book> store = new ArrayList<>();
public void addBook(final Book book) {
store.add(book);
}
public List<Book> findBooks(final Date from, final Date to) {
Calendar end = Calendar.getInstance();
end.setTime(to);
end.roll(Calendar.YEAR, 1);
return store.stream().filter(book -> {
return from.before(book.getPublished()) && end.getTime().after(book.getPublished());
}).sorted(Comparator.comparing(Book::getPublished).reversed()).collect(Collectors.toList());
}
}
Adding a Test Runner for jUnit
We need only one annotation to make the stuff work with jUnit: Cucumber as jUnit runner. I have put the steps, the test-class and the feature-file in similar packages so that the cucumbers automatic scan is working but this behaviour can be overwritten with @CucumberOptions e.g. @CucumberOptions(features=\{“path-to-features”}..).
package feature.book;
import org.junit.runner.RunWith;
import cucumber.api.junit.Cucumber;
@RunWith(Cucumber.class)
public class BookSearchTest {
}
Adding Steps
This is our glue code containing the step definitions. It’s a stateful class that matches steps from the feature description with their parameters to states in our application. The @Format annotation allows us to convert dates in a specific date format into a date parameter.
package feature.book;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.hascode.tutorial.cucumber.book.Book;
import com.hascode.tutorial.cucumber.book.Library;
import cucumber.api.Format;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
public class BookSearchSteps {
Library library = new Library();
List<Book> result = new ArrayList<>();
@Given(".+book with the title '(.+)', written by '(.+)', published in (.+)")
public void addNewBook(final String title, final String author, @Format("dd MMMMM yyyy") final Date published) {
Book book = new Book(title, author, published);
library.addBook(book);
}
@When("^the customer searches for books published between (\\d+) and (\\d+)$")
public void setSearchParameters(@Format("yyyy") final Date from, @Format("yyyy") final Date to) {
result = library.findBooks(from, to);
}
@Then("(\\d+) books should have been found$")
public void verifyAmountOfBooksFound(final int booksFound) {
assertThat(result.size(), equalTo(booksFound));
}
@Then("Book (\\d+) should have the title '(.+)'$")
public void verifyBookAtPosition(final int position, final String title) {
assertThat(result.get(position - 1).getTitle(), equalTo(title));
}
}
Example 2: Salary Manager
In the following example, we’ll be using cucumbers data table import to map data from an ASCII table into our domain model.
Writing the Feature
Again we’re starting with a description of the desired behaviour. To enter multiple employees, we’re using a data table as shown here:
Feature: Salary Management
Scenario: Modify an employee's salary
Given the salary management system is initialized with the following data
| id | user | salary |
| 1 | donald | 60000.0 |
| 2 | dewie | 62000.0 |
| 3 | goofy | 55000.0 |
| 4 | scrooge | 70000.0 |
| 5 | daisy | 56000.0 |
| 6 | minnie | 62000.0 |
| 7 | mickey | 51000.0 |
| 8 | fethry | 66500.0 |
When the boss increases the salary for the employee with id '3' by 5%
Then the payroll for the employee with id '3' should display a salary of 57750
Domain Objects
Again we have two classes: employees and the salary manager.
Employee
An employee has an id, a user-name and a salary, encapsulated in this simple POJO:
package com.hascode.tutorial.cucumber.salary;
public class Employee {
private int id;
private String user;
private float salary;
// constructor, getter, setter ommitted
}
SalaryManager
The salary manager allows to increase an employee’s salary and to find an employee by his id. The list of know employees is passed as constructor parameters so that the simple for-tutorials-only implementation looks like this:
package com.hascode.tutorial.cucumber.salary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class SalaryManager {
private Map<Integer, Employee> employees = new HashMap<>();
public SalaryManager(final List<Employee> employees) {
this.employees = employees.stream().collect(Collectors.toMap(Employee::getId, Function.<Employee> identity()));
}
public void increaseSalary(final Integer id, final int increaseInPercent) {
Employee nominee = employees.get(id);
float oldSalary = nominee.getSalary();
nominee.setSalary(oldSalary + oldSalary * increaseInPercent / 100);
}
public Employee getPayroll(final int id) {
return employees.get(id);
}
}
Adding a Test Runner for jUnit
Again we’re adding this starting point for jUnit:
package feature.salary;
import org.junit.runner.RunWith;
import cucumber.api.junit.Cucumber;
@RunWith(Cucumber.class)
public class SalaryTest {
}
Adding Steps
We’re running the tests, see them fail, have a look in the console and copy and paste the generated cucumber code. Afterwards we’re implementing the rest until we seen the green light ;)
An interesting feature is the ability to map the data table into a collection of a mappable element. The first method annotated with @Given receives a generic list of employees as a parameter so that the field of the data table are mapped to their equivalent fields in the employee class.
package feature.salary;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import java.util.List;
import com.hascode.tutorial.cucumber.salary.Employee;
import com.hascode.tutorial.cucumber.salary.SalaryManager;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
public class SalarySteps {
SalaryManager manager;
@Given("^the salary management system is initialized with the following data$")
public void the_salary_management_system_is_initialized_with_the_following_data(final List<Employee> employees) throws Throwable {
manager = new SalaryManager(employees);
}
@When("^the boss increases the salary for the employee with id '(\\d+)' by (\\d+)%$")
public void the_boss_increases_the_salary_for_the_employee_with_id_by(final int id, final int increaseInPercent) throws Throwable {
manager.increaseSalary(id, increaseInPercent);
}
@Then("^the payroll for the employee with id '(\\d+)' should display a salary of (\\d+)$")
public void the_payroll_for_the_employee_with_id_should_display_a_salary_of(final int id, final float salary) throws Throwable {
Employee nominee = manager.getPayroll(id);
assertThat(nominee.getSalary(), equalTo(salary));
}
}
Directory Structure
The directory structure with the examples described above looks like this one:
.
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── hascode
│ │ └── tutorial
│ │ └── cucumber
│ │ ├── book
│ │ │ ├── Book.java
│ │ │ └── Library.java
│ │ └── salary
│ │ ├── Employee.java
│ │ └── SalaryManager.java
│ └── resources
└── test
├── java
│ └── feature
│ ├── book
│ │ ├── BookSearchSteps.java
│ │ └── BookSearchTest.java
│ └── salary
│ ├── SalarySteps.java
│ └── SalaryTest.java
└── resources
└── feature
├── book
│ └── search_book.feature
└── salary
└── salary_management.feature
Running the Tests
Now we’re ready to run the tests – using Maven it’s done like this:
$ mvn test
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running feature.book.BookSearchTest
1 Scenarios (1 passed)
7 Steps (7 passed)
0m0.122s
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.375 sec
Running feature.salary.SalaryTest
1 Scenarios (1 passed)
3 Steps (3 passed)
0m0.018s
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.033 sec
Results :
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
Otherwise we can use our favourite IDE’s jUnit Runner – this is what running the tests looks like in Eclipse IDE:
Cucumber and Java EE
Integrating Cucumber in a Java EE project is quite easy when using Arquillian and the Cukespace extension – please feel free to have a look at another article of mine for further reading: "Marrying Java EE and BDD with Cucumber, Arquillian and Cukespace".
Alternative: jBehave
JBehave is an alternative framework for writing BDD tests using the Gherkin syntax an with annotation driven mapping between the text format of a story and the glue code to make the tests work. I’ve written an article a while ago about this framework so please feel free to have a look if interested: "Oh JBehave, Baby! Behaviour Driven Development using JBehave".
Tutorial Sources
Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:
git clone https://github.com/hascode/cucumber-java-tutorial.git
Other BDD Articles of mine
The following articles of mine are covering different aspects and frameworks for Behaviour Driven Development:
Article Updates
-
2015-01-07: Link to my article about Cucumber, Java EE and Cukespace added.
-
2017-04-06: Links to other BDD articles of mine added.