Data tables in Cucumber 3

Filed under: Cucumber, — Tags: Cucumber-JVM, Data tables — Thomas Sundberg — 2018-06-30

Version 3 of Cucumber for Java has recently been released. It brings Cucumber expressions to Cucumber-JVM. It also brings a new implementation of Data tables.

From a Gherkin perspective, nothing has changed. Data tables are supported as earlier. As a developer you need to adapt the steps a bit.

Let's start with a small example and then progressively create something more complicated.

Convert a one-column table to a List

A good starting point could be this scenario where a list of numbers are summed.

Feature: Cucumber can convert a Gherkin data table 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 table in the example above can be converted to a List<Integer> that can be used in a step.

When Cucumber is executed, it will print these snippets as a suggestion for a starting point for steps that haven't been implemented yet:

You can implement missing steps with the snippets below:

@Given("a list of numbers")
public void a_list_of_numbers(DataTable dataTable) {
    // Write code here that turns the phrase above into concrete actions
    // For automatic transformation, change DataTable to one of
    // List<E>, List<List<E>>, List<Map<K,V>>, Map<K,V> or
    // Map<K, List<V>>. E,K,V must be a String, Integer, Float,
    // Double, Byte Short, Long, BigInteger or BigDecimal.
    //
    // For other transformations you can register a DataTableType.
    throw new PendingException();
}

@When("I summarize them")
public void i_summarize_them() {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

@Then("should I get {int}")
public void should_I_get(Integer int1) {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

The most interesting snippet is the first one, the one that suggest that the argument to the method is a DataTable dataTable. The snippet suggests that you should replace the DataTable dataTable argument with any of:

It also tells us that each type, E, K, V must be of any of these types:

One thing to notcie is that Cucumber 3 supports more types than Cucumber 2.

An example of a parameter declaration is List<E> which could become List<Integer>.

An implementation could like this:

package se.thinkcode;

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 is similar to how it was implemented in Cucumber 2. Cucumber 3 supports a few more types (String, Integer, Float, Double, Byte, Short, Long, BigInteger, and BigDecimal).

It isn't very complicated to convert a one-column data table into a list of numbers. Let's continue with an example where a two-column data table is converted into a Map.

Convert a two-column data table to a Map

Let's look at a price list like the one below:

Feature: Cucumber can convert a Gherkin table to to a map.

  This an example of a 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 I order 1 donut
    Then should I pay 3

We can see that there are two types of items and that they have a price. If we want to convert this to a data table, we declare a Map<K, V> like this: Map<String, Integer> and use it in a step. The implementation may look like this:

package se.thinkcode;

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 PriceListSteps {
    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) {
        this.priceList = priceList;
    }

    @When("I order {int} {word}")
    public void i_order(Integer numberOfFirstItems, String item) {
        int firstPrice = priceList.get(item);

        totalSum += firstPrice * numberOfFirstItems;
    }

    @Then("should I pay {int}")
    public void should_I_pay(int expectedCost) {
        assertThat(totalSum, is(expectedCost));
    }
}

Each row in the data table is converted to an item in the map. The order of the rows is not important. The keys in the When step are used to find the price in the dictionary priceList.

Next step is to extend the price list with currency. This will turn the data table into a three-column list and introduce a need for a type that isn't supported out of the box.

Convert a three-column table data table into a custom type

It would be nice to get the price list as a list of price items. It could be declared like this: List<Price>. The type Price is not any of the built-in types. It has to be implemented and registered so cucumber can convert the data table to it. The need to register the type is new for Cucumber 3.

Let's start with a feature with this new, complicated, price list.

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 I buy 1 donut
    Then should I pay 1 EUR and 18 SEK

The new type Price has to be implemented. This is one possible implementation:

package se.thinkcode;

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;
    }
}

It is an immutable type with three fields. The fields match the headers in the data table. The fields and headers doesn't have to match. But as they describe the same thing it feels natural that they have the same name in this case.

The next step is new for Cucumber 3. The type has to registered before it can be used in a data table. It is done like this:

package se.thinkcode;

import cucumber.api.TypeRegistry;
import cucumber.api.TypeRegistryConfigurer;
import io.cucumber.datatable.DataTableType;

import java.util.Locale;
import java.util.Map;

public class PriceTransformer implements TypeRegistryConfigurer {
    public Locale locale() {
        return Locale.ENGLISH;
    }

    public void configureTypeRegistry(TypeRegistry typeRegistry) {
        typeRegistry.defineDataTableType(new DataTableType(Price.class,
                        (Map<String, String> row) -> {
                            String product = row.get("product");
                            String currency = row.get("currency");
                            Integer price = Integer.parseInt(row.get("price"));

                            return new Price(product, price, currency);
                        }
                )
        );
    }
}

You may notice that the order of the fields in the constructor are mixed compared to the order in the data table in the feature. It doesn't matter since each row is converted to a map where each value is finally retrieved.

The registration of your own type will be simplified in upcoming releases of Cucumber-JVM.

With this infrastructure prepared, it is time to do the actual implementation of the steps that uses this data table. It looks like this:

package se.thinkcode;

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) {
        priceList = new HashMap<>();

        for (Price price : prices) {
            String key = price.getProduct();
            priceList.put(key, price);
        }
    }

    @When("I buy {int} {word}")
    public void i_buy(Integer numberOfItems, String item) {
        Price price = priceList.get(item);
        calculate(numberOfItems, price);
    }

    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 {int} EUR and {int} SEK")
    public void should_I_pay_EUR_and_SEK(Integer expectedEuroSum, Integer expectedSekSum) {
        assertThat(euroSum, is(expectedEuroSum));
        assertThat(sekSum, is(expectedSekSum));
    }
}

Migrating from Cucumber 2 to Cucumber 3 will force you to register your custom types if you use custom types in your steps. This breaks backwards compatibility. But the upside is that the Cucumber project was able to get rid of a dependency, XStream that was poorly understood and not needed anymore.

Runner and project file

The Cucumber runner used to execute the steps looks like this:

package se.thinkcode;

import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
public class RunCukesTest {
}

The dependencies are specified in a Maven pom that looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.thinkcode.cucumber</groupId>
    <artifactId>cucumber-3-datatables</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>3.0.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>3.0.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgument>-Werror</compilerArgument>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Conclusion

As a developer, there is a bit more work you might have to do. You need to register a transformer. But the magic that was used earlier has now been removed.

You can, and probably should, use your favorite transformation library. If you don't use any transformation library, it is possible to do the transformation yourself as you saw above.

Acknowledgements

I would like to thank Malin Ekholm and Aslak Hellesøy for feedback.

Resources



(less...)

Pages

About
Events
Why

Categories

Agile
Automation
BDD
Clean code
Continuous delivery
Continuous deployment
Continuous integration
Cucumber
Culture
Design
Executable specification
Gradle
Guice
J2EE
JUnit
Java
Linux
Load testing
Maven
Mockito
Pair programming
PicoContainer
Presentation
Programming
Public speaking
Quality
Recruiting
Requirements
Scala
Selenium
Software craftsmanship
Software development
Spring
TDD
Teaching
Technical debt
Test automation
Tools
Web
Windows
eXtreme Programming

Authors

Thomas Sundberg
Adrian Bolboaca

Archives

Meta

rss RSS