Cucumber-JVM Parameter & Datatable Type Conversion

Introduction

XStream conversions was removed in version 3.0.0, ParameterType and DataTableType registration were introduced. DataTable handling was simplified by adding default transformers for TableEntry and TableCell in version 4.0.0. Anonymous Parameter Types was added in version 4.2.0. This has been made even easier in version 5 by adding annotations for registering type conversions and default transformers.

The feature files used here is the same as used in the articles explaining migration to ParameterType and DataTableType in 3.x from 2.x, DataTableType default object mapper in 4.0 and anonymous ParameterTypes in 4.2.

The techniques mentioned below will work for Cucumber version 5 series and beyond.

The source code for this article can be found here. The POJO’s contain annotations from the Lombok project.

Transformer Registration Changes

It is no longer mandatory to define ParameterType and DataTableType in the configuration class that implements TypeRegistryConfigurer interface. These can be located inside the step definition and hook classes, or for that matter inside any package defined in the glue option. These registration methods need to be annotated with ParameterType or DataTableType. This also means that the test data is also available to these methods. One can have a combination of the version 4 registrations and the new annotated style. Or go all the way and use annotations exclusively.

Default transformers, which use object mappers like Jackson, for ParameterType and DatatableType can now be registered by using the following annotations – DefaultParameterTransformer, DefaultDataTableEntryTransformer and DefaultDataTableCellTransformer. There is no need to create a transformer class that implements ParameterByTypeTransformer, TableEntryByTypeTransformer or TableCellByTypeTransformer interfaces.

When the configuration class that implements TypeRegistryConfigurer interface is not used then the locale is picked up from the language mentioned in the feature file.

The doc string parameter can be converted into an object by using the DocStringType annotation on the transformer method.

ParameterType Annotations

The @ParameterType annotation is the shorthand way of registering ParameterTypes. Below is a step with transformation from a comma delimited string to a list of User objects.

Given the users are jane,john,colin,alice

This is the way it is handled in version 4. (TypeRegistery, StepDefinition)

//Step Definition
@Given("the users are {users}")
public void givenUsers(List<User> names) { }

//Registration TypeRegistryConfigurer
registry.defineParameterType(new ParameterType<>("users", ".*?", List.class,
    (String s) -> Arrays.asList(s.split(",")).stream().map(User::new).collect(Collectors.toList())));

In version 5+, the step definition method remains the same. (StepDefinition)

@ParameterType(".*?")
public List<User> users(String name) {
    return Arrays.asList(name.split(",")).stream().map(User::new).collect(Collectors.toList());
}

The parameter type (users) maps to the annotated method name. If the method name is different then this mapping can be added to the ‘name’ option of the ParameterType annotation. The regex pattern is mapped to the default value of the ParameterType annotation. The return type and the transformer logic placement is self explanatory.

In case of a CaptureGroupTransformer, an array is no longer the parameter to the transformer.  Individual parameters are used instead. Below is a step requiring such a transformation to a Money object.

Given the total payment is in £ main currency 825 fractional currency 91

This is the way it is handled in version 4. (TypeRegistery, StepDefinition)

//Step Definition
@Given("the total payment is in {currency}")
public void the_total_payment_is_in(Money money) {  }

//Registration TypeRegistryConfigurer
registry.defineParameterType(new ParameterType<>("currency", 
    "(.) main currency ([\\d]+) fractional currency ([\\d]+)", Money.class,
    (String[] args) -> new Money(args[0], Integer.parseInt(args[1]), Integer.parseInt(args[2]))));

In version 5+, the step definition method remains the same. (StepDefinition)

@ParameterType("(.) main currency ([\\d]+) fractional currency ([\\d]+)")
public Money currency(String symbol, String main, String frac) {
    return new Money(symbol, Integer.parseInt(main), Integer.parseInt(frac));
}

It is pretty easy to work with anonymous parameter types and will be explained in the default transformer annotation section. The remaining transformer registrations and step definitions for the scenarios in the feature file can be looked up in the linked locations.

DataTableType Annotations

The @DataTableType annotation is the shorthand way of registering DataTableTypes.

TableEntryTransformer

This DataTable to collection of objects transformer, is used when the table has a header row which maps to the instance fields. Below is a scenario with transformation from a DataTable to a List of Lecture objects.

Given the list lecture details are
| profName | topic         | size | frequency | rooms     |
| Jack     | A1:Topic One  | 40   | 3         | 101A,302C |
| Daniels  | B5:Topic Five | 30   | 2         | 220E,419D |

This is the way it is handled in version 4. (TypeRegistery, StepDefinition)

//Step Definition
@Given("the list lecture details are")
public void theLectureDetailsAre(List<Lecture> lectures) { }

//Registration TypeRegistryConfigurer
registry.defineDataTableType(new DataTableType(Lecture.class, new TableEntryTransformer<Lecture>() {
    @Override
    public Lecture transform(Map<String, String> entry) {
        return Lecture.createLecture(entry);
    }
}));

In version 5+, the step definition method remains the same. (StepDefinition) The DataTableType registration can be done as follows.

@DataTableType
public Lecture getLecture(Map<String, String> entry) {
    return Lecture.createLecture(entry);
}
TableCellTransformer

This is a cell value to a object transformer. Below is a scenario with transformation from a DataTable to a Map of Lecture objects with LectureId object as key.

Given the map lecture details are
|   | profName | topic         | size | frequency | rooms     |
| 1 | John Doe | A1:Topic One  | 40   | 3         | 101A,302C |
| 2 | Jane Doe | B5:Topic Five | 30   | 2         | 220E,419D |

This is the way it is handled in version 4. (TypeRegistery, StepDefinition) This will also use a transformation for the Lecture object, the registration for which is shown in the above section.

//Step Definition
@Given("the map lecture details are")
public void theMapLectureDetailsAre(Map<LectureId, Lecture> lectures) {  }

//Registration TypeRegistryConfigurer
registry.defineDataTableType(new DataTableType(LectureId.class, new TableCellTransformer<LectureId>() {
    @Override
    public LectureId transform(String cell) {
        return new LectureId(Integer.parseInt(cell));
    }
}));

In version 5+, the step definition method remains the same. (StepDefinition) The DataTableType registration can be done as follows.

@DataTableType
public LectureId getLectureId(String cell) throws Throwable {
    return new LectureId(Integer.parseInt(cell));
}
TableRowTransformer

This DataTable to collection of object transformer, is used when the table has no header row. Below is a scenario with transformation from a DataTable to a List of LectureLite objects.

Given the list no header lecture details are
| John Doe | A1:Topic One  | 40 | 3 | 101A,302C |
| Jane Doe | B5:Topic Five | 30 | 2 | 220E,419D |

This is the way it is handled in version 4. (TypeRegistery, StepDefinition)

//Step Definition
@Given("the list no header lecture details are")
public void theListNoHeaderLectureDetailsAre(List<LectureLite> lectures) { }

//Registration TypeRegistryConfigurer
registry.defineDataTableType(new DataTableType(LectureLite.class, new TableRowTransformer<LectureLite>() {
    @Override
    public LectureLite transform(List<String> row) {
        return LectureLite.createLectureLite(row);
    }
}));

In version 5+, the step definition method remains the same. (StepDefinition) The DataTableType registration can be done as follows.

@DataTableType
public LectureLite getLectureLite(List<String> row) {
    return LectureLite.createLectureLite(row);
}
TableTransformer

This DataTable to a single object transformer, is used when the table needs to be converted to a single object. Below is a scenario with transformation from a DataTable to a List of Lectures object.

Given all lectures details
| profName | topic          | size | frequency | rooms     |
| John     | A1:Topic One   | 40   | 3         | 101A,302C |
| Jane     | Z9:Topic Six   | 30   | 2         | 220E,419D |
| Patrick  | E5:Topic Two   | 60   | 1         | 901B,732C |
| Melrose  | M6:Topic Seven | 20   | 2         | 444E,909A |

This is the way it is handled in version 4. (TypeRegistery, StepDefinition)

//Step Definition
@Given("all lectures details")
public void allLecturesDetails(Lectures lectures) {  }

//Registration TypeRegistryConfigurer
registry.defineDataTableType(new DataTableType(Lectures.class, new TableTransformer<Lectures>() {
    @Override
    public Lectures transform(DataTable table) {
        List<Lecture> lects = table.asMaps().stream().
            map(m -> Lecture.createLecture(m)).collect(Collectors.toList());
	return new Lectures(lects);
    }
}));

In version 5+, the step definition method remains the same. (StepDefinition) The DataTableType registration can be done as follows.

@DataTableType
public Lectures getLectures(DataTable table) {
    List<Lecture> lects = table.asMaps().stream().
        map(m -> Lecture.createLecture(m)).collect(Collectors.toList());
    return new Lectures(lects);
}

Default Transformer Annotations

Most of the transformations can be handled by default transformers – @DefaultParameterTransformer, @DefaultDataTableEntryTransformer, @DefaultDataTableCellTransformer – without registering any explicit ParameterType or DataTableType transformers. An object mapper like Jackson is required for using this functionality. These default transformers need to be registered. This does not work for TableRowTransformer and TableTransformer.

The method of handling this in version 4 is as below. (TypeRegistery)

private static final class JacksonTransformer implements ParameterByTypeTransformer, 
    TableEntryByTypeTransformer, TableCellByTypeTransformer {

    //Anonymous Parameter
    public Object transform(String s, Type type) {  }

    //Default TableEntryByTypeTransformer
    public <T> T transform(Map<String, String> entry,Class<T> type,TableCellByTypeTransformer cellTransformer) {}

    //Default TableCellByTypeTransformer
    public <T> T transform(String value, Class<T> cellType) {  }
}

public class Configurer implements TypeRegistryConfigurer {
    public void configureTypeRegistry(TypeRegistry registry) {
	JacksonTransformer jacksonTransformer = new JacksonTransformer();
	registry.setDefaultParameterTransformer(jacksonTransformer);
	registry.setDefaultDataTableEntryTransformer(jacksonTransformer);
        registry.setDefaultDataTableCellTransformer(jacksonTransformer);
    }
}

This has been simplified in version 5+ with the default transformer annotations. (DefaultTransformer)

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

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

Here the default transformer registration is in a separate class but it works inside any package in the glue option.

This now takes care of the Anonymous ParameterTypes without requiring any registration. Examples of transformation without ParameterType registration – object (Scenario, StepDefinition), enum (Scenario, StepDefinition), BigInteger (Scenario, StepDefinition).

This also handles the DataTableTypes for TableEntryTransformer and TableCellTransformer. The transformer registration for TableEntryTransformer (Lecture) and TableCellTransformer (LectureId) from the previous section can be removed. Examples of transformation without DataTableType registration – primitive instance variables (Scenario, StepDefinition), enum instance variables (Scenario, StepDefinition), object instance variables (Scenario, StepDefinition).

DocStringType Annotation

This is a new feature introduced in version 5, which allows the DocString in a step to be transformed into an object (Speech) by registering a transformer using the @DocStringType annotation. (StepDefinition)

Given the doc string is
"""
Hello there how r u?
Doing great.
Whats new?
Nothing much.
"""

@DocStringType
public Speech speechDetails(String text) {
    return new Speech(text);
}

@Given("the doc string is")
public void the_doc_string_is(Speech speech) { }

2 thoughts on “Cucumber-JVM Parameter & Datatable Type Conversion”

  1. Hi ,
    Add extent report dependencies “extentreports-cucumber5-adapter” from one of your projects to this cucumber 5 data table project and it started giving parser error after “mvn install” removing the extent reported plugin works fine.
    As per my understanding, the extent report is failing for @ParameterType
    Class ParameterV5Definition
    @ParameterType
    Correct me, if my understanding is wrong.

    Error:
    Running parameter.ParameterConvertTest
    Configuring TestNG with: org.apache.maven.surefire.testng.conf.TestNG652Configurator@490d6c15
    io.cucumber.testng.TestNGCucumberRunner
    WARNING: By default Cucumber is running in –non-strict mode.
    This default will change to –strict and –non-strict will be removed.
    You can use –strict or @CucumberOptions(strict = true) to suppress this warning
    Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.738 sec <<< FAILURE!
    setUpClass(parameter.ParameterConvertTest) Time elapsed: 0.596 sec <<< FAILURE!
    io.cucumber.core.exception.CucumberException: java.lang.NoClassDefFoundError: gherkin/ast/Node
    at io.cucumber.core.plugin.PluginFactory.newInstance(PluginFactory.java:85)
    at io.cucumber.core.plugin.PluginFactory.instantiate(PluginFactory.java:68)
    at io.cucumber.core.plugin.PluginFactory.create(PluginFactory.java:55)
    at io.cucumber.core.plugin.Plugins.createPlugins(Plugins.java:48)
    at io.cucumber.core.plugin.Plugins.(Plugins.java:25)
    at io.cucumber.testng.TestNGCucumberRunner.(TestNGCucumberRunner.java:106)
    at io.cucumber.testng.AbstractTestNGCucumberTests.setUpClass(AbstractTestNGCucumberTests.java:22)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)

    com.aventstack
    extentreports-cucumber5-adapter
    1.0.0
    test

    com.aventstack
    extentreports
    4.0.9
    test

Leave a Reply

Your email address will not be published. Required fields are marked *