Filed under: Cucumber, Programming, — Tags: BDD, Behaviour Driven Development - BDD, Cucumber, Cucumber DataTable, Cucumber-jvm, DataTable, JUnit, Java, Maven, Test automation — Thomas Sundberg — 2014-06-30
Cucumber has a nice feature that will help you to use tables in your scenarios. The table can easily be converted to a list or a map that you can use in your step. I will show you a few examples that may help you get started.
A first example of a scenario with a table could be this:
Scenario: The sum of a list of numbers should be calculated Given a list of numbers | 17 | | 42 | | 4711 | When I summarize them Then should I get 4770
This table can easily be converted to a List<Integer>
that you can use in your step.
When you execute your feature without the step defined, you will get this snippet to help you:
You can implement missing steps with the snippets below: @Given("^a list of numbers$") public void a_list_of_numbers(DataTable arg1) throws Throwable { // Write code here that turns the phrase above into concrete actions // For automatic transformation, change DataTable to one of // List<YourType>, List<List<E>>, List<Map<K,V>> or Map<K,V>. // E,K,V must be a scalar (String, Integer, Date, enum etc) throw new PendingException(); }
The DataTable arg1
can be replaced by a List<Integer> numbers
that can be used in
the step.
A complete example may look like:
src/test/resources/se/thinkcode/Sum.feature
Feature: Cucumber can convert Gherkin data tables to a list of a type you specify. Scenario: The sum of a list of numbers should be calculated Given a list of numbers | 17 | | 42 | | 4711 | When I summarize them Then should I get 4770
The corresponding implementation could look like this:
src/test/java/se/thinkcode/steps/ArithmeticSteps.java
package se.thinkcode.steps; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import java.util.List; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; public class ArithmeticSteps { private List<Integer> numbers; private int sum; @Given("^a list of numbers$") public void a_list_of_numbers(List<Integer> numbers) throws Throwable { this.numbers = numbers; } @When("^I summarize them$") public void i_summarize_them() throws Throwable { for (Integer number : numbers) { sum += number; } } @Then("^should I get (\\d+)$") public void should_I_get(int expectedSum) throws Throwable { assertThat(sum, is(expectedSum)); } }
This example converted a table with scalars to a list of Integers. Let me show an example where I convert a table to a Map.
We need to specify a Map<K, V>
. The types must match the columns in the table. It could look like
this:
src/test/resources/se/thinkcode/SimplePriceList.feature
Feature: Cucumber can convert a Gherkin table to to a map. This an example of a simple price list. Scenario: A price list can be represented as price per item Given the price list for a coffee shop | coffee | 1 | | donut | 2 | When I order 1 coffee and 1 donut Then should I pay 3
The corresponding implementation could look like this:
src/test/java/se/thinkcode/steps/SimplePriceList.java
package se.thinkcode.steps; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import java.util.Map; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; public class SimplePriceList { private Map<String, Integer> priceList; private int totalSum; @Given("^the price list for a coffee shop$") public void the_price_list_for_a_coffee_shop(Map<String, Integer> priceList) throws Throwable { this.priceList = priceList; } @When("^I order (\\d+) (.*) and (\\d+) (.*)$") public void i_order_coffee_and_donut(int numberOfFirstItems, String firstItem, int numberOfSecondItems, String secondItem) throws Throwable { int firstPrice = priceList.get(firstItem); int secondPrice = priceList.get(secondItem); totalSum += firstPrice * numberOfFirstItems; totalSum += secondPrice * numberOfSecondItems; } @Then("^should I pay (\\d+)$") public void should_I_pay(int expectedCost) throws Throwable { assertThat(totalSum, is(expectedCost)); } }
The interesting part is the implementation of the_price_list_for_a_coffee_shop(Map<String, Integer>
priceList)
. The table will be converted to a map with a String
as key and Integer
as the value. I use this map as a dictionary when I calculate the total sum to be paid for a coffee and a donut.
The example above show how a relatively simple table can be converted. If it would be a real price list, I would
like to see the currency used. Let me extend the example to include a currency. This will allow me to introduce a
custom type that the DataTable
converter will be able to use.
We are unfortunately not able to convert a custom type to a Map
. But we can solve this by using a
List<YourType>
and then convert this to a map that is easier to use as a dictionary. But let me
start with the feature before we look into the step implementation.
src/test/resources/se/thinkcode/InternationalPriceList.feature
Feature: Cucumber can convert a Gherkin table to to a map. This an example of a more complicated price list. Scenario: An international coffee shop must handle currencies Given the price list for an international coffee shop | product | currency | price | | coffee | EUR | 1 | | donut | SEK | 18 | When I buy 1 coffee and 1 donut Then should I pay 1 EUR and 18 SEK
The price list now handles currencies. It is a very strange coffee shop that uses different currencies for different
products. But it allows me to introduce a custom type that can be automatically converted to a List
that can be converted to a Map
. I want it as a map so I can use it as a dictionary when I use it.
My custom type is implemented as
src/test/java/se/thinkcode/steps/Price.java
package se.thinkcode.steps; public class Price { private String product; private Integer price; private String currency; public Price(String product, Integer price, String currency) { this.product = product; this.price = price; this.currency = currency; } public String getProduct() { return product; } public Integer getPrice() { return price; } public String getCurrency() { return currency; } }
The steps are finally implemented as:
src/test/java/se/thinkcode/steps/InternationalPriceList.java
package se.thinkcode.steps; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; public class InternationalPriceList { private Map<String, Price> priceList; private int sekSum; private int euroSum; @Given("^the price list for an international coffee shop$") public void the_price_list_for_an_international_coffee_shop(List<Price> prices) throws Throwable { priceList = new HashMap<String, Price>(); for (Price price : prices) { String key = price.getProduct(); priceList.put(key, price); } } @When("^I buy (\\d+) (.*) and (\\d+) (.*)$") public void i_order_coffee_and_donut(int numberOfFirstItems, String firstItem, int numberOfSecondItems, String secondItem) throws Throwable { Price firstPrice = priceList.get(firstItem); calculate(numberOfFirstItems, firstPrice); Price secondPrice = priceList.get(secondItem); calculate(numberOfSecondItems, secondPrice); } private void calculate(int numberOfItems, Price price) { if (price.getCurrency().equals("SEK")) { sekSum += numberOfItems * price.getPrice(); return; } if (price.getCurrency().equals("EUR")) { euroSum += numberOfItems * price.getPrice(); return; } throw new IllegalArgumentException("The currency is unknown"); } @Then("^should I pay (\\d+) EUR and (\\d+) SEK$") public void should_I_pay_EUR_and_SEK(int expectedEuroSum, int expectedSekSum) throws Throwable { assertThat(euroSum, is(expectedEuroSum)); assertThat(sekSum, is(expectedSekSum)); } }
I have to convert the list with my custom type to a map manually. Converting the list to a map will simplify how I can use it later.
The table contains a headline with product, currency and price. The order is different compared to the order of the fields in my Price class. This difference is handled by Cucumber when converting the table.
The file structure used to implement this example is
example |-- pom.xml `-- src |-- main | `-- java `-- test |-- java | `-- se | `-- thinkcode | |-- RunCukesTest.java | `-- steps | |-- ArithmeticSteps.java | |-- InternationalPriceList.java | |-- Price.java | `-- SimplePriceList.java `-- resources `-- se `-- thinkcode |-- InternationalPriceList.feature |-- SimplePriceList.feature `-- Sum.feature
There are two files left to show, the Maven pom and the runner I use to run the example. The Maven pom looks like this:
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <groupId>se.thinkcode.blog</groupId> <artifactId>example</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.1.7</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-junit</artifactId> <version>1.1.7</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> </dependencies> </project>
It is small and only contains the dependencies needed for Cucumber and JUnit.
To run the example, I need something that will run Cucumber. I use the JUnit runner. The implementation looks like this:
src/test/java/se/thinkcode/RunCukesTest.java
package se.thinkcode; import cucumber.api.junit.Cucumber; import org.junit.runner.RunWith; @RunWith(Cucumber.class) public class RunCukesTest { }
Using tables may increase the expressiveness when writing a scenario. Use them whenever you need. An alternative to use a table may sometimes be to create a scenario outline.
I wish to thank Johan Helmfrid and Malin Ekholm for the feedback.