Cucumber-jvm 3 migration of DATATABLE conversion from version 2

Introduction

XStream in Java Cucumber implementation is dead. No more trying to decide whether to extend AbstractSingleValueConverter or implement Converter. This has been replaced by the concept of ParameterType and DataTableType.

For the official announcement mentioning other goodies, wander to this link – https://cucumber.io/blog/2018/05/19/announcing-cucumber-jvm-3-0-0.

For the release notes navigate to – https://github.com/cucumber/cucumber-jvm/blob/master/CHANGELOG.md and scroll down to the 3.0.0-SNAPSHOT section.

For details on Cucumber Expressions which have been introduced to work alongside Regular Expressions refer to this – https://docs.cucumber.io/cucumber/cucumber-expressions/

Source Code

The migrated code for version 3.x is located here. The code for version 2.x is located here.

DataTableType

In Cucumber 2.x, XStream automatically handles the conversion of a datatable into the desired collection like List or Map. One of the requirements is that the datatable headers match the instance variables of the class in the collection. For example the step – the list primitive lecture details are – with a datatable is to be converted into a LecturePrimitive object. The step definition has the following pattern – ^the list primitive lecture details are$.

Given the list primitive lecture details are
	| profName | size | profLevel  |
	| Jane     |   40 | Assistant  |
	| Doe      |   30 | Associate  |

Now if we use this same piece of code in Cucumber 3 we will get this error.

cucumber.runtime.CucumberException: Could not convert arguments for step [the list primitive lecture details are] defined at ......... It appears you did not register a data table type 

Let us look at Cucumber 3 code by adding a DataTableType for conversion. One can find the complete code for the stepstep definitiondestination class, transformation method and parameter registration at the linked locations

public class Configurer implements TypeRegistryConfigurer {
    @Override
    public void configureTypeRegistry(TypeRegistry registry) {
	registry.defineDataTableType(new DataTableType(LecturePrimitive.class, new TableEntryTransformer<LecturePrimitive>() {
            @Override 
            public LecturePrimitive transform(Map<String, String> entry) { 
                return LecturePrimitive.createLecture(entry); 
            } 
        }));
    }
}

It is very important that the class Configurer needs to be placed inside the package structure mentioned in the glue option given inside @CucumberOptions. Registration of all ParameterType and DataTableType will happen inside the configureTypeRegistry method.

What is DataTableType? – This contains the transformation code for converting the table cell, row or whole table to the mentioned object. This takes the place of XStream conversion but now we also need to take care of table data conversion to objects which were earlier automatic.

Let us look at DataTableType constructor in more detail. TableTransformer is just a placeholder for specific transformer; it is not an interface or abstract class.

DataTableType
LecturePrimitive.class     ->     Desired object class
TableTransformer           ->     Transformation code

This will now output the same result as in the earlier cucumber versions.

TableTransformers

There are four types of TableTransformer – TableEntryTransformer, TableRowTransformer, TableCellTransformer, TableTransformer.

Transformer Type		Parameter passed to transform()		Usage scenarios
TableEntryTransformer		Map<String, String>			Transform DataTable containing header
TableRowTransformer		List<String>				Transform DataTable without header
TableCellTransformer		String					Transform a single cell into object
TableTransformer		DataTable				Transform a whole table

In the above method, the TableEntryTransformer is used because the datatable has headers. For each row of the datatable, a Map is created with the header as keys and respective column in the row as values. This Map is passed to the transform method in which the Map details are converted into the destination class.

Below are some of the common conversions of DatatTable to a Java collection with migration details mentioned.

DataTable TO List of list of primitives

There is no need to write code for Cucumber 2.x or Cucumber 3,x. This will be handled automatically. The Cucumber 2.x code details – step and step definition. The Cucumber 3.x code details – step and step definition.

Given the list string lecture details are
| Jane | 40 | Assistant |
| Doe  | 30 | Associate |

@Given("the list string lecture details are")
public void theListStringLectureDetailsAre(List<List<String>> lectures) { }

DataTable TO List of objects with primitive fields

This is the case mentioned in the DataTableType section above. The destination class variables are all primitives. This is handled automatically in Cucumber 2.x. The Cucumber 2.x code details – stepstep definition and destination class. In Cucumber 3.x, a custom transformer has to be written for the DataTableType. The Cucumber 3.x code details – stepstep definitiondestination class, transformation method and parameter registration at the linked locations. The TableEntryTransformer is used for this conversion.

public class LecturePrimitive {
    private String profName;	
    private int size;		
    private String profLevel;
}

Given the list primitive lecture details are
| profName | size | profLevel |
| Jane     | 40   | ASSISTANT |
| Doe      | 30   | ASSOCIATE |

@Given("the list primitive lecture details are")
public void thePrimitiveLectureDetailsAre(List<LecturePrimitive> lectures) {}

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

DataTable TO List of objects with primitive and enum fields

The destination class variables contain an enum and primitives. This is handled automatically in Cucumber 2.x. The Cucumber 2.x code details – stepstep definitiondestination class and enum. In Cucumber 3.x, a custom transformer has to be written for the DataTableType. The Cucumber 3.x code details – stepstep definitiondestination class, enum, transformation method and parameter registration at the linked locations.  The TableEntryTransformer is used for this conversion.

public class LecturePrimitiveEnum {
    private String profName;
    private int size;
    private ProfLevels profLevel;
}

public enum ProfLevels { ASSISTANT, ASSOCIATE, PROFESSOR }

Given the list primitive enum lecture details are
| profName | size | profLevel  |
| Jane     |   40 | Assistant  |
| Doe      |   30 | Associate  |

@Given("the list primitive enum lecture details are")
public void thePrimitiveEnumLectureDetailsAre(List<LecturePrimitiveEnum> lectures) {}

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

DataTable TO List of objects with primitive and object and enum fields

The destination class variables contain an object, enum and primitives. This is handled automatically in Cucumber 2.x. The Cucumber 2.x code details – stepstep definitiondestination class, contained object class and enum. In Cucumber 3.x, a custom transformer has to be written for the DataTableType. The Cucumber 3.x code details – stepstep definitiondestination classcontained object classenum, transformation method and parameter registration at the linked locations. The TableEntryTransformer is used for this conversion.

public class LectureSimple {
    private Professor profName;
    private int size;
    private ProfLevels profLevel;
}

public class Professor {
    private String profName;
}

public enum ProfLevels { ASSISTANT, ASSOCIATE, PROFESSOR }

Given the list simple lecture object details are
| profName | size | profLevel |
| Jane     |   40 | ASSISTANT |
| Doe      |   30 | ASSOCIATE |

@Given("the list simple lecture object details are")
public void theSimpleLectureObjectDetailsAre(List<LectureSimple> lectures) {}

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

DataTable TO List of objects with other objects as fields

This is where things get interesting. In Cucumber 2.x, one could write the code in the stepdefinition method or work with XStream to convert to the object. The Cucumber 2.x code details – stepstep definitiondestination class, contained object classes – Professor, Topic, Rooms, Room and XStreamConverters –  ProfessorXStreamConverterTopicXStreamConverter, RoomsXStreamConverter.

In Cucumber 3.x, a custom transformer has to be written for the DataTableType. The Cucumber 3.x code details – stepstep definitiondestination class, contained object classes – Professor, Topic, Rooms, Roomtransformation method and parameter registration at the linked locations. The TableEntryTransformer is used for this conversion.

public class Lecture {
    private Professor profName;
    private Topic topic;
    private int size;
    private int frequency;
    private Rooms rooms;
}

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 |

Given("^the lecture details are$")
public void theLectureDetailsAre(List<Lecture> lectures) {}

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

DataTable without header TO List of Object (or any other collection)

In Cucumber 2.x, the way out is to accept a List<List> as a parameter to the stepdefinition method and write the conversion code to the desired collection. The Cucumber 2.x code details – stepstep definition and destination class.

In Cucumber 3.x, we need to add a DataTableType with the TableRowTransformer for conversion which handles DataTable without headers. For each row of the datatable, a String List is created which contains the row cells values. This List is passed to the transform method in which the List details are converted into the destination class.

The Cucumber 3.x code details – stepstep definitiondestination class, transformation method and parameter registration at the linked locations. We are using LectureLite in place of Lecture as the same class cannot be registered with different transformers.

public class LectureLite {
    private Professor profName;
    private Topic topic;
    private int size;
    private int frequency;
    private Rooms rooms;
}

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 |

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

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

DataTable with 2 columns TO Map with primitive key and value

There is no need to write code for Cucumber 2.x or Cucumber 3,x. This will be handled automatically. The Cucumber 2.x code details – step and step definition. The Cucumber 3.x code details – step and step definition.

Given the map primitive key value
| Jane | Assistant |
| John | Associate |

@Given("the map primitive key value")
public void theMapPrimitiveKeyValue(Map<String,String> profs) {}

DataTable with 2 columns TO Map with custom object key and value

In Cucumber 2.x, XStream will automatically convert it using the single argument constructor or we can set up a XStream converter. The Cucumber 2.x code details – stepstep definition and destination classes – Professor and ProfLevels.

In Cucumber 3.x, we need to register new DataTableType using a TableCellTransformer. The String value of the cell is sent to the transform method where it can be converted. The Cucumber 3.x code details – stepstep definition, destination classes – ProfessorProfLevels and parameter registration –  ProfessorProfLevels at the linked locations.

Given the map object key value
| Jane | ASSISTANT |
| John | ASSOCIATE |

@Given("the map object key value")
public void theMapObjectKeyValue(Map<Professor,ProfLevels> profs) { }

registry.defineDataTableType(new DataTableType(Professor.class, new TableCellTransformer<Professor>() {
@Override
public Professor transform(String cell) throws Throwable {
        return new Professor(cell);
    }
}));

registry.defineDataTableType(new DataTableType(ProfLevels.class, new TableCellTransformer<ProfLevels>() {
@Override
public ProfLevels transform(String cell) throws Throwable {
    return ProfLevels.valueOf(cell.toUpperCase());
    }
}));

DataTable with more than 2 columns TO Map with primitive key and custom object value

In Cucumber 2.x, the stepdefinition method needs to accept a List<List> as a parameter and write the conversion code to a Map with a String key from the first column. Notice that the DataTable does not have headers. The Cucumber 2.x code details – stepstep definition and destination class.

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

In Cucumber 3.x, this is handled with the registration of a DataTableType. Only requirement for this automatic conversion to a Map is the first column of the DataTable will be considered as the key if there is no header. This will be the first row if the @Transpose annotation is used for a table with vertical headers.

The Cucumber 3.x code details – stepstep definitiondestination classtransformation method and parameter registration at the linked locations. The TableEntryTransformer is used for this conversion.

Given the map primitive key 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 |

@Given("the map primitive key lecture details are")
public void theMapPrimitiveKey(Map<String,Lecture> lectures) { }

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

DataTable with more than 2 columns TO Map with custom object key value

In Cucumber 2.x, the stepdefinition method needs to accept a List<List> as a parameter and write the conversion code to a Map with an object key created from the first column . Notice that the DataTable does not have headers. The Cucumber 2.x code details – stepstep definition and destination class.

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

In Cucumber 3.x, the key creation is handled with the registration of a DataTableType using the TableCellTransformer from the first column. The value creation is handled by the registration of a DataTableType using the TableEntryTransformer. Only requirement for this automatic conversion to a Map is the first column of the DataTable will be considered as the key if there is no header. This will be the first row if the @Transpose annotation is used for a table with vertical headers.

The Cucumber 3.x code details – stepstep definitiondestination classtransformation method and parameter registration – LectureId, Lecture at the linked locations.

public class LectureId {
    private int id;
}
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 |

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

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

DataTable TO a Single object

In Cucumber 2.x, the stepdefinition method needs to accept a List<List> or DatatTable as a parameter and write the conversion code to the desired object. The Cucumber 2.x code details – stepstep definition and destination class.

In Cucumber 3.x, we need register a new DataTableType with the TableTransformer for conversion to the desired class. The Cucumber 3.x code details – stepstep definitiondestination class and parameter registration at the linked locations.

public class Lectures {
    private List<Lecture> lectures;
}

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 |

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

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

Transpose DataTable TO converting to a list or map

If the headers are switched from horizontal to vertical that is, the table is transposed, add the @Transpose annotation in the stepdefinition method. Check the step and stepdefinition for conversion to a List. Check the step and stepdefinition for conversion to a Map.

Leave a Reply

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