Let us have the following feature file,
Feature: Search Employees
Background:
Given following employees exists
| id | name | department |
| 1 | Jack | HR |
| 2 | Rachel | Finance |
| 3 | Mike | HR |
| 4 | Emma | IT |
Scenario: Get Employees By Department
Given user wants to get list employees in a department
When searched for department = 'HR'
Then following list of employees are returned
| id | name | department |
| 1 | Jack | HR |
| 3 | Mike | HR |
Imagine, following step calls a REST endpoint which returns a JSON.
When searched for department = 'HR'
Here is the repose JSON,
[
{
"id": 1,
"name": "Jack",
"department": "HR"
},
{
"id": 3,
"name": "Mike",
"department": "HR"
}
]
Corresponding Java Class,
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Employee {
private Integer id;
private String name;
private String department;
}
In older version of cucumber (ie 1.2.4), we can do DataTable.diff(List<Map<String, String> actual) as below,
@Then("^following list of employees are returned$")
public void following_list_of_employees_are_returned(DataTable expectedEmployees) throws Throwable {
List<Map<String, Object>> actualEmployees = new ArrayList<>();
List<Employee> employees = response.as(Employee[].class);
employees
.forEach(e -> {
Map<String, Object> map = new HashMap<>();
map.put("id", e.getId());
map.put("name", e.getName());
map.put("department", e.getDepartment());
actualEmployees.add(map);
});
expectedEmployees.unorderedDiff(actualEmployees);
}
Currently, we upgraded to following cucumber version,
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java8</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>4.0.0</version>
<scope>test</scope>
</dependency>
[INFO] +- io.cucumber:cucumber-java8:jar:4.0.0:test
[INFO] | +- io.cucumber:cucumber-java:jar:4.0.0:test
[INFO] | \- net.jodah:typetools:jar:0.5.0:test
[INFO] +- io.cucumber:cucumber-spring:jar:4.0.0:test
[INFO] \- io.cucumber:cucumber-junit:jar:4.0.0:test
[INFO] \- io.cucumber:cucumber-core:jar:4.0.0:test
[INFO] +- io.cucumber:cucumber-html:jar:0.2.7:test
[INFO] +- io.cucumber:gherkin:jar:5.1.0:test
[INFO] +- io.cucumber:tag-expressions:jar:1.1.1:test
[INFO] +- io.cucumber:cucumber-expressions:jar:6.1.0:test
[INFO] \- io.cucumber:datatable:jar:1.1.3:test
[INFO] \- io.cucumber:datatable-dependencies:jar:1.1.3:test
PROBLEM: In cucumber 1.2.4 versions, DataTable can be diff'ed with a List<Map<String, String>. In the newer version (4.0.0), DataTable.diff expects a DataTable as argument and there is no method to support diff'ing List.
Now, we need to create a datatable object from List<Map<String, String>. so that we can do expectedDataTable.diff(actualDataTable).
QUESTION: Is there a easy way to convert Array of JSON Object or List<JavaObject> to a DataTable so that we can do diff of 2 datatables without creating List<List<String>> from list of objects which requires a lot of code.
Full disclosure: I wrote the data table module for Cucumber.
Manually mapping objects from and to data tables is time consuming, boring and error prone. This is best left to an object mapper. Additionally the implementation used to compare a table to List<Map<String, String>>
contained magic, flamethrowers and gotchas. So I thought it best to leave it out.
Solution 1
The first thing you want to do is upgrade to v4.2.0.
Then put the following configuration somewhere on the glue path. The object mapper is from Jackson. It usually comes with Spring.
public class ParameterTypes implements TypeRegistryConfigurer {
@Override
public Locale locale() {
return ENGLISH;
}
@Override
public void configureTypeRegistry(TypeRegistry typeRegistry) {
Transformer transformer = new Transformer();
typeRegistry.setDefaultDataTableCellTransformer(transformer);
typeRegistry.setDefaultDataTableEntryTransformer(transformer);
typeRegistry.setDefaultParameterTransformer(transformer);
}
private class Transformer implements ParameterByTypeTransformer, TableEntryByTypeTransformer, TableCellByTypeTransformer {
ObjectMapper objectMapper = new ObjectMapper();
@Override
public Object transform(String s, Type type) {
return objectMapper.convertValue(s, objectMapper.constructType(type));
}
@Override
public <T> T transform(Map<String, String> map, Class<T> aClass, TableCellByTypeTransformer tableCellByTypeTransformer) {
return objectMapper.convertValue(map, aClass);
}
@Override
public <T> T transform(String s, Class<T> aClass) {
return objectMapper.convertValue(s, aClass);
}
}
}
Then replace @Getter
and @Setter
with@Data
so hashcode
, equals
and toString
are all implemented.
@Data
public class Employee {
private Integer id;
private String name;
private String department;
}
Then modify your step to use a list of employees instead of a data table. The object mapper installed in the previous step will handle the transform from data table to objects.
@Then("^following list of employees are returned$")
public void following_list_of_employees_are_returned(List<Employee> expectedEmployees) throws Throwable {
List<Map<String, Object>> actualEmployees = new ArrayList<>();
List<Employee> employees = response.as(Employee[].class);
assertEquals(expectedEmployees, actualEmployees);
}
To make the comparison order insensitive consider using AssertJs assertThat
instead of JUnits assertEquals
- it usually comes with Spring
Solution 2
Add datatable-matchers
to your dependencies
<groupId>io.cucumber</groupId>
<artifactId>datatable-matchers</artifactId>
Create your own data table and compare it using the DataTableHasTheSameRowsAs
matcher.
@Then("^following list of employees are returned$")
public void following_list_of_employees_are_returned(DataTable expectedEmployees) {
List<Employee> employees = response.as(Employee[].class);
DataTable actualEmployees = createTable(
employees,
asList("id", "name", "department"),
Employee::getId, Employee::getName, Employee::getDepartment
);
assertThat(actualEmployees, hasTheSameRowsAs(expectedEmployees));
}
static <T> DataTable createTable(List<T> values, List<String> headers, Function<T, Object>... extractors) {
List<List<String>> rawTable = new ArrayList<>();
rawTable.add(headers);
values.stream()
.map(employee -> Stream.of(extractors)
.map(f -> f.apply(employee))
.map(String::valueOf)
.collect(Collectors.toList()))
.forEach(rawTable::add);
return create(rawTable);
}