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.
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
.
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),
* ]
*/
}
}
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;
}
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")));
}
}
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
.
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 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$"
.
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(){
}
}
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.
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
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).
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
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.
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.
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.
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.
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 ]
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 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.