BDD Testing with Cucumber, Java and JUnit
December 28th, 2014 by Micha KopsWhether 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.
Contents
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 Bitbucket repository, fork it there or clone it using Git:
git clone https://bitbucket.org/hascode/cucumber-java-tutorial.git
Resources
Other BDD Articles of mine
The following articles of mine are covering different aspects and frameworks for Behaviour Driven Development:
- Oh JBehave, Baby! Behaviour Driven Development using JBehave
- Writing BDD-Style Webservice Tests with Karate and Java
- A short Introduction to ScalaTest
- Running JavaScript Tests with Maven, Jasmine and PhantomJS
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.
Tags: attd, bdd, cucumber, gherkin, Java, jbehave, maven, sbe, tdd, test, testing
February 6th, 2015 at 7:02 am
Hi,
I have tried to replicate this tutorial to understand the concepts, but I am having some challenges. Can anyone please assist me. The last 3 lines in the Library.jave file are giving problems: Here are the 3 lines and the problems:
Java Lines
———-
return store.stream().filter(book -> {
return from.before(book.getPublished()) && end.getTime().after(book.getPublished());
}).sorted(Comparator.comparing(Book::getPublished).reversed()).collect(Collectors.toList());
Problems
——–
Description Resource Path Location Type
Syntax error on tokens, delete these tokens Library.java /BookClub/src/cucumberfeatures line 22 Java Problem
Syntax error on tokens, Expression expected instead Library.java /BookClub/src/cucumberfeatures line 22 Java Problem
Syntax error on token(s), misplaced construct(s) Library.java /BookClub/src/cucumberfeatures line 21 Java Problem
after cannot be resolved or is not a field Library.java /BookClub/src/cucumberfeatures line 21 Java Problem
book cannot be resolved Library.java /BookClub/src/cucumberfeatures line 21 Java Problem
Syntax error on tokens, delete these tokens Library.java /BookClub/src/cucumberfeatures line 20 Java Problem
book cannot be resolved to a variable Library.java /BookClub/src/cucumberfeatures line 20 Java Problem
The method stream() is undefined for the type List Library.java /BookClub/src/cucumberfeatures line 20 Java Problem
February 7th, 2015 at 7:57 pm
Hi,
did you use Java 8? Nevertheless I’ve added a version using Java 7 for you in a branch named jdk7 here: https://bitbucket.org/hascode/cucumber-java-tutorial/src/ed3b7a1a738265ecbbb30f65d363fa3c8c5cd9be/?at=jdk7
Cheers
Micha
March 4th, 2015 at 1:48 am
Great stuff! Exactly what the cukes.info site is missing in their documentation. I found it really difficult to get started with cukes/gherkin/junit/java until I read this and now I feel confident to use it, and can happily refer back to cukes.info for more details. Good solid examples that introduce various concepts with tangible simple, but non-trivial, examples.
Many thanks!
March 4th, 2015 at 5:49 am
Thanks for your kind remarks! I’m glad that I could be a help for you :)
July 6th, 2015 at 3:33 am
Thanks for the detailed explanation of the steps. However i am not able to run the tests using ‘mvn test’. Please help in resolving this. Also can you please help me to run it via JUnit.
The output is as below –
——————————————————-
T E S T S
——————————————————-
There are no tests to run.
Results :
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
July 6th, 2015 at 4:26 am
Hi Srikant,
did you add cucumber-junit as a dependency to your project and have you added @RunWith(Cucumber.class) to your test class?
July 17th, 2015 at 2:45 pm
Thanks a lot for that post. It was very interesting.
July 17th, 2015 at 7:58 pm
Hi Dahuchao,
thanks, you’re welcome! :)
October 1st, 2015 at 10:41 am
very interesting, but unfortunately the code doesn’t cater for the edge case where the Book is published on the 1st of January in the start year for the range to search.
return store.stream().filter(book -> {
return (from.before(book.getPublished()) || from.equals(book.getPublished())) && end.getTime().after(book.getPublished());
}).sorted(Comparator.comparing(Book::getPublished).reversed()).collect(Collectors.toList());
This corrects for the Start data I did not check the edge case for the end date
December 18th, 2015 at 5:30 pm
Crisp and nice article to get a java dev upto pace with Cucumber really fast! Thanks and Love from New York.
December 23rd, 2015 at 7:19 am
I’m working with some Agile user story folks atm … I think I’ll sell them on Cuke pretty quick ;)
December 23rd, 2015 at 1:33 pm
Thanks, you’re welcome! :)
January 29th, 2016 at 6:40 am
Just what I wanted….this was a great tutorial…thank you!
January 29th, 2016 at 8:27 am
Hi Vishal,
thanks, you’re welcome!
Cheers
Micha
March 22nd, 2016 at 9:41 am
Thanks, this post helped me a lot
March 22nd, 2016 at 6:57 pm
Thanks, you’re welcome!
July 1st, 2016 at 5:20 am
Hi Sir,
i run the Example but it gives me Error:lambda expressions are not supported in -source 1.5
(use -source 8 or higher to enable lambda expressions).im using jdk 1.8.0_31
Please help so.
Thanks in Advance.
Harshal
July 1st, 2016 at 5:43 am
Hi Harshal.S,
as I am using Java 8′s streams API in my examples, you need to run my examples with Java 8 and need to tell Maven to use Java 8.
Please feel free to have a look at the project’s pom.xml (especially the settings for the maven compiler plugin) https://bitbucket.org/hascode/cucumber-java-tutorial/src/382f30ca59d40382a76af575a0e08b6aedccfca6/pom.xml?fileviewer=file-view-default
August 21st, 2016 at 7:06 pm
Hello Micha,
This is an amazing example for beginners like me on Cucumber-junit.
Though i could not understand the exact flow so posting below question.
Please let me know if you get a chance
BookSearchTest.java class finds feature file by @RunWith(Cucumber.class)
But how does the BookSearchSteps.java gets trigger to perform all the @given,@when and @then test?
Thank you very much in advance
August 24th, 2016 at 3:53 pm
Great article. It is something to be necessary to start use cucumber with java.
August 25th, 2016 at 4:37 am
Thanks! You’re welcome :)
October 28th, 2016 at 3:11 am
Very nice article… simple and concise… Thank you so much
October 28th, 2016 at 5:51 am
Thanks – you’re welcome! :)