Skip to content

Latest commit

 

History

History
639 lines (497 loc) · 20.6 KB

v5.0.0.md

File metadata and controls

639 lines (497 loc) · 20.6 KB

Cucumber-JVM v5.0.0

This release was made possible by significant contributions and test driving from - in no particular order; Anton Deriabin, Toepi, Alexandre Monterroso, Ralph Kar, Marit Van Dijk, Tim te Beek, Yatharth Zutshi, David Goss, Dominic Adatia, Marc Hauptmann, John Patrick, Vincent Psarga, Luke Hill, Konrad M, Michiel Leegwater, and Loïc Péron.

We'll cover some of notable changes since v4. 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. With cucumber-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 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.

package com.example;

public class StepDefinitions {

    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 specify 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.

Scenario:  Some information isn't known yet
  Given some great authors
    | Full Name      | Born       | Died       | 
    | Terry Pratchet | 1948-04-28 | 2015-03-12 |
    | Neil Gaiman    | 1948-04-28 |            |
package com.example;

import java.time.LocalDate;

public class Author {
   public String fullName;
   public LocalDate born;
   public LocalDate died;
}
package com.example;

import io.cucumber.java.en.Given;

import java.util.List;

public class StepDefinitions {
    
    @Given("some great 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),
        * ]
        */
    }
}

Empty cells in data tables

As mentioned in the previous sections. Empty cells in a data table are converted to null values rather then the empty string. However there are use cases where an empty string is actually desired.

By declaring a table transformer with a replacement string it becomes‌ ‌possible to explicitly disambiguate between the two cases. For‌ ‌example: ‌

Given some authors
   | name            | first publication |
   | Aspiring Author |                   |
   | Ancient Author  | [blank]           |
package com.example.app;

import io.cucumber.java.DataTableType;
import io.cucumber.java.en.Given;

import java.util.List;

public class StepDefinitions {

    @DataTableType(replaceWithEmptyString = "[blank]")
    public Author convert(Map<String, String> entry){
      return new Author(
         entry.get("name"),
         entry.get("first publication")
      );
    }
    
    @Given("some authors")
    public void given_some_authors(List<Author> authors){
      // authors = [Author(name="Aspiring Author", firstPublication=null), 
      //              Author(name="Ancient Author", firstPublication=)]
    }
}

To do the same for List<String>, Map<String, List<String>>, ect use a table cell converter that will convert [blank] to the empty string.

@DataTableType(replaceWithEmptyString = "[blank]")
public String convert(String cell){
   return cell;
}

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
package com.example.app;

import io.cucumber.java.fr.Étantdonné;

import java.util.List;

public class StepDefinitions {
    
    @É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.

package com.example;

import io.cucumber.docstring.DocString;

public class StepDefinitions {

    @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.

package com.example;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.cucumber.java.DocStringType;
import io.cucumber.java.en.Given;

public class StepDefinitions {

    @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.

package com.example;

import io.cucumber.java.en.Given;

public class StepDefinitions {

    @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, and 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:

  • JUnit 5 Assertions.assertTimeout*
  • Awaitility
  • Guava TimeLimiter

Sharing the Spring Application Context

The cucumber-spring module provides dependency injection using the Spring application context. The application context can take a long time to start up so Springs TestContextManager framework will share identical application contexts between tests.

However to use step definitions Cucumber has to modify the application context. When executing in parallel step definitions were registered concurrently and this resulted in several race conditions. So as a workaround Cucumber would create a new application context for each thread.

Some sleuthing by Dominic Adatia uncovered the root cause of the race conditions and a solution to solve it properly. As an additional benefit Cucumber will also share the application context with other unit tests improving performance somewhat more.

Deprecating --non-strict

In most frameworks tests can either be skipped, failed, or succeed. In Cucumber, they can also be pending or undefined. Tests that are skipped or succeeded do not fail the build. But depending on your opinion of work-on-in-progress, pending and undefined test might.

Cucumber facilitates provides the --strict and --non-strict execution options which makes work in progress fail or pass respectively. This flexibility comes at a cost. Tools that interpet Cucumbers output also have to be configured with either the --strict or the --non-strict option and these configurations have to be consistent.

While --non-strict has been the default for a long time we are now of the opinion that work in progress is a failing state. This means that in proper TDD fashion when given a feature file, it will not pass until all steps have been implemented and made to pass (note: Cucumbers generated step definitions throw a pending exception). To make this transition graceful Cucumber will log a warning when using --non-strict. The warning can be suppressed by using --strict. Eventually we'll remove the --non-strict option and make --strict the default behaviour.

JUnit 5 Support

Cucumber-JVM now has JUnit5 support. To use it add the cucumber-junit-platform-engine dependency to your project. ‌

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-junit-platform-engine</artifactId>
    <version>${cucumber.version}</version>
    <scope>test</scope>
</dependency>

You can provide options by adding a junit-platform.properties file to your classpath root. Below are the supported options. ‌

cucumber.ansi-colors.disabled=                          # true or false. default: false
cucumber.execution.dry-run=                             # true or false. default: false
cucumber.glue=                                          # comma separated package names. example: com.example.glue
cucumber.plugin=                                        # comma separated plugin strings. example: pretty, json:path/to/report.json
cucumber.object-factory=                                # object factory class name. example: com.example.MyObjectFactory
cucumber.snippet-type=                                  # underscore or camelcase. default: underscore
cucumber.execution.parallel.enabled=                    # true or false. default: false
cucumber.execution.parallel.config.strategy=            # dynamic, fixed or custom. default: dynamic
cucumber.execution.parallel.config.fixed.parallelism=   # positive integer. example: 4
cucumber.execution.parallel.config.dynamic.factor=      # positive double. default: 1.0
cucumber.execution.parallel.config.custom.class=        # class name. example: com.example.MyCustomParallelStrategy

Altogether your project should look like this. ‌

├─ pom.xml or build.gradle
├─ src/main/java/
|  └─ com/example/app/
|     └─ Application.java
├─ src/test/java/
|  └─ com/example/app/
|     └─ ApplicationStepDefinitions.java
└─ src/test/resources/
   ├─ junit-platform.properties
   └─ com/example/app/
      └─ application.feature

‌ For more details see the junit-platform-engine/README.md.

Tooling Support

So Cucumber has JUnit5 support. What does this actually mean and how do I run my features? JUnit5 consists of three parts. The JUnit Platform, JUnit Jupiter, and JUnit Vintage. The JUnit Platform is a framework to develop test engines. Examples of these would be JUnit Jupiter and JUnit Vintage.

Cucumber implements the JUnit Platform API. Any one using Cucumber and the JUnit Platform will be able to discover, filter and execute feature files as if they were just another test. As Cucumber now implements a popular API it should be easier to integrate with build systems and IDEs.

Running with the Console Launcher

The JUnit 5 project provides a ConsoleLauncher. You can use this to run Cucumber. See the JUnit5 documentation for details. This will ouput something like this. ‌

╷
└─ Cucumber ✔
   ├─ A feature with scenario outlines ✔
   │  ├─ A scenario ✔
   │  ├─ A scenario outline ✔
   │  │  ├─ With some text ✔
   │  │  │  ├─ Example #1 ✔
   │  │  │  └─ Example #2 ✔
   │  │  └─ With some other text ✔
   │  │     ├─ Example #1 ✔
   │  │     └─ Example #2 ✔
   │  └─ A scenario outline with one example ✔
   │     └─ Examples ✔
   │        ├─ Example #1 ✔
   │        └─ Example #2 ✔
   └─ A feature with a single scenario ✔
      └─ A single scenario ✔

Test run finished after 2588 ms
[         8 containers found      ]
[         0 containers skipped    ]
[         8 containers started    ]
[         0 containers aborted    ]
[         8 containers successful ]
[         0 containers failed     ]
[         8 tests found           ]
[         0 tests skipped         ]
[         8 tests started         ]
[         0 tests aborted         ]
[         8 tests successful      ]
[         0 tests failed          ]

Maven Surefire and Gradle

While Maven Surefire and Gradle support the JUnit Platform they do not yet use it for test discovery1,2,3. So a work around is needed. Add this marker class to the package containing your feature files. Then run your build system as you would normally ‌

package com.example.app;

import io.cucumber.junit.platform.engine.Cucumber;

@Cucumber
public class RunCucumberTest {
}

‌Now your project should look like this: ‌

├─ pom.xml or build.gradle
├─ src/main/java/
|  └─ com/example/app/
|     └─ Application.java
├─ src/test/java/
|  └─ com/example/app/
|     ├─ ApplicationStepDefinitions.java
|     └─ RunCucumberTest.java
└─ src/test/resources/
   ├─ junit-platform.properties
   └─ com/example/app/
      └─ application.feature

IntelliJ IDEA and Eclipse

IntelliJ IDEA and Eclipse have some support for the JUnit Platform4. Currently you can select package and run all features in it. It is not yet possible to select single files or scenarios. This may become easier once support for file based test engines improves5.