With close to 300 commits since the last release of Cucumber v4 we're ready to release v5.0.0-RC1. This release was made possible by significant contributions from - in no particulair order; Anton Deriabin, Toepi, Ralph Kar, Marit van Dijk, Tim te Beek, Yatharth Zutshi, John Patrick, Vincent Psarga, Luke Hill, Konrad M and Michiel Leegwater.

This release is a release candidate - as indicated by the -RC1 suffix. This means that while not final this release is indicative of what v5.0.0 will be. If there are no major changes or findings in the next four weeks we'll finalize and release v5.0.0.  

We'll cover some of notable changes in this release here. As always the full changelog can be found in the repository.

Annotation based Configuration

Cucumber Expressions were originally introduced in Cucumber-JVM 3.0.0. With it came the ability to register parameter- and data table-types by implementing the TypeRegistryConfigurer.

The TypeRegistryConfigurer however is not part of the glue. This made it impossible to access the test context. Withcucumber-java this is now possible by using the @ParameterType, @DataTableType and @DocStringType annotations. This allows parameter-, data table- and docstring types to be mapped to objects which can only be created by services inside the test context.

For example in this scenario

Given the awesome catalog
When a user places the awestruck eels in his basket
Then you will be shocked at what happened next

We are now able to look up the "awestruck eels" in the "awesome" catalog as part of the parameter transform.

private final Catalog catalog; 
private final Basket basket;

@ParameterType("[a-z ]+")
public Catalog catalog(String name) {
  return catalogs.findCatalogByName(name);
}

@ParameterType("[a-z ]+")
public Product product(String name) {
  return catalog.findProductByName(name);
}

@Given("the {catalog} catalog")
public void the_catalog(Catalog catalog){
  this.catalog = catalog
}

@When("a user places the {product} in his basket")
public void a_user_place_the_product_in_his_basket(Product product){
  basket.add(product);
}

Note: The method name is used as the parameter name. A parameter name can also be provided via the name property of @ParameterType.

Default Transformer

It is now also possible to register default transformers using annotations. Default transformers allow you to specificy a transformer that will be used when there is no data table or parameter type defined. This can be combined with an object mapper like Jackson to quickly transform string representations into objects.

The available default transformers are:

  • @DefaultParameterTransformer
  • @DefaultDataTableEntryTransformer
  • @DefaultDataTableCellTransformer

Typically you'd use them all at once.

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
import io.cucumber.java.DefaultDataTableCellTransformer;
import io.cucumber.java.DefaultDataTableEntryTransformer;
import io.cucumber.java.DefaultParameterTransformer;

import java.lang.reflect.Type;

public class DataTableSteps {

   private final ObjectMapper objectMapper = 
       new ObjectMapper().registerModule(new JSR310Module());

   @DefaultParameterTransformer
   @DefaultDataTableEntryTransformer
   @DefaultDataTableCellTransformer
   public Object defaultTransformer(Object fromValue, Type toValueType) {
  JavaType javaType = objectMapper.constructType(toValueType)
  return objectMapper
         .convertValue(fromValue, javaType);
   }
}

To facilitate the transformation of data table entries the table headers are converted from title case to camel case and empty cells are  represented by null values rather then empty strings. So the following is now possible.

public class Author {
   public String fullName;
   public LocalDate born;
   public LocalDate died;
}

@Given("some authors")
public void some_authors(List<Author> authors){
   /*
    * authors = [
    *   Author(fullName="Terry Pratchet", born=1948-04-28, died=2015-03-12)
    *    Author(fullName="Neil Gaiman", born=1960-11-10, died=null),
    * ]
    */
}

Localization

Some languages uses commas rather then points to separate decimals. To parse these properly you'd have to use TypeRegistryConfigurer.locale to set this globally. Cucumber will now use the language from the feature file unless a locale is explicitly provided by the TypeRegistryConfigurer. This makes the following work without additional configuration.

# language: fr
Fonctionnalité: Concombres fractionnaires

  Scénario: dans la ventre
    Étant donné j'ai 5,5 concombres fractionnaires
@Étantdonné("j'ai {bigdecimal} concombres fractionnaires")
public void jAiConcombresFractionnaires(BigDecimal arg0) {
    assertThat(arg0, is(new BigDecimal("5.5")));
}

DocString

In addition to tables Gherkin supports doc strings.

Given some more information
  """json
  { 
     "produce": "Cucumbers",
     "weight": "5 Kilo", 
     "price": "1€/Kilo"
  }
  """

In Cucumber v4 these were treated as 1x1 data tables. Cucumber v5 introduces a dedicated DocString object and type registry.

@Given("some more information")
public void some_more_information(DocString docString){
    String content = docString.getContent(); // { "produce": "Cucumber" ....
    String contentType = docString.getContentType(); // json
}

And using @DocStringType annotation it is possible to define transformations to other object types.

@DocStringType
public JsonNode json(String docString) throws IOException {
    return objectMapper.readTree(docString);
}
    
@Given("some more information")
public void some_more_information(JsonNode json){

}

Cucumber will first attempt to convert a doc string by looking for a doc string type that matches the content type. If none is available then Cucumber will attempt to use the parameter type of the annotated method.

Note: The method name is used as the content type. Content type can also be provided via the contentType property of @DocStringType.

Property based options

It is possible to pass properties to cucumber using CLI arguments in a property. For instance:

mvn clean test -Dcucumber.options="--strict --monochrome"

This is rather complicated, esp when multiple shells are involved and the quotes get confusing:

mvn clean test -Dcucumber.options='--strict --monochrome --tags "not @ignored"'

So a better way to do this is to provide each option individually:

mvn clean test -Dcucumber.execution.strict=true -Dcucumber.ansi-colors.disabled=true -Dcucumber.filter.tags="not @ignored"

A full list of properties can be found in: Constants.java

Cucumber Expressions

Cucumber-Expressions would originally try to guess if you wanted to use regular of cucumber expressions. While helpful this also made it much harder to understand if a particular string would be evaluated as a regular- or as a cucumber expression.

From now on a regular expression is any string that starts with ^ and/or ends with $. Everything else is considered a Cucumber Expression. This means that "there is a (.*) step" will no longer be seen as a regular expression. This expression should be rewritten to "there is a {} step" or "^there is a (.*) step$".

Repeatable annotations

All step definition annotations (@Given, @When, ect) are now repeatable.

@Given("a step definition")
@Given("another way to express the same thing")
public void a_step_definition(){

}

New package structure

With the introduction of the module system in Java 9 it is no longer possible to use the same package in different jar files. However different components of Cucumber would use cucumber.api to mark their public API and Cucumber also relied on split packages to detect extensions.

The split packages have been removed after a significant refactoring. All packages are now rooted in io.cucumber.<module> and Backend and ObjectFactory implementations are loaded via SPI.

This was unfortunately a significant breaking change that affects both step definition and the plugin APIs. The changes to the step definitions were already introduced in version v4.5.0 to allow a graceful migration.

It was not possible to do the same with the plugin system. So plugins written for Cucumber v4 will not work with Cucumber v5 but we have taken this as an opportunity to use the JSR310 classes for timestamps and duration.

Public API

Prior to v5 cucumber used the cucumber.api package to mark its public API. This resulted in an a structure that exposed more implementation details then strictly necessary. Replacing it with @API Guardian annotations allows for a more encapsulated structure and a better defined API. A typical user of Cucumber with dependencies on

  • cucumber-java
  • cucumber-junit
  • cucumber-pico

will now only need to import classes from

  • io.cucumber.java
  • io.cucumber.junit
  • io.cucumber.datatable
  • io.cucumber.docstring

Dependency changes

The refactoring also resulted in some changes in the dependency structure:

  • cucumber-java8 no longer depends on cucumber-java.
  • cucumber-pico, cucumber-spring and other DI modules no longer depend on cucumber-java.

If you did not declare a dependency on cucumber-java you may have to add one now.

Finally the Plugin API was extracted to it's own module cucumber-plugin. Custom plugin implementations will now only need to depend on this module rather then all of Cucumber and its dependencies. Best used with scope compileOnly (Maven) or compileOnly (Gradle).

Removing timeout

It was possible to provide a timeout to step definitions. Unfortunately the semantics are complicated. Cucumbers implementation would attempt to interrupt the long running step but would not stop if the step was stuck indefinitely. Additionally Cucumber would not consider a step failed if it did not terminate within the given timeout.

To remove the confusion and complexity we removed timeout from Cucumber. Consider replacing this functionality with the features provided by one of these libraries instead: