The goal of this lib is to perform automatic mapping between gherkins datatable and Java bean. It provides some annotations to describe the mapping.
Two main features are provided:
- validate the datatable (validate that all headers are defined on bean)
- perform the mapping
It's available on maven central repository. You can find it here io.github.deblockt:cucumber-datatable-to-bean-mapping
This lib support only java 17+.
Using this bean:
@DataTableWithHeader // this annotation is used to register this class as a Datatable
class Bean {
@Column("column 1") // this annotation register this var as a column of the datatable
public String column1;
@Column(mandatory = false) // the column value is optional. If not specified the field name will be used
public String column2;
}
// compatible with records
@DataTableWithHeader // this annotation is used to register this class as a Datatable
record Bean(
@Column("column 1") // this annotation register this var as a column of the datatable
String column1,
@Column(mandatory = false) // the column value is optional. If not specified the field name will be used
String column2
) { }
And this step definition:
// java style
@Given("a step with a datatable")
public void stepWithList(List<Bean> beans) {
System.out.println("read: " + beans);
}
// java-8 style
Given(
"a step with a datatable",
(DataTable datatable) -> System.out.println("read: " + datatable.asList(Bean.class))
);
You can write this step:
Then a step with a datatable
| column 1 | column2 |
| value1 | value2 |
| value3 | |
And That's all. The mapping is performed automatically.
The following parameters are available on the @Column
annotation. The @Column
annotation can be omitted, see the field resolution chapter.
name | type | description |
---|---|---|
value | string[] | the column name. You can specify multiple name for the same column. The default value is builded from the field name. see name resolution chapter |
description | string | the column description. The description is displayed when a datatable is malformed to show a helper message. |
mandatory | boolean | default true. Can be set to false to set the column to optional |
defaultValue | string | the default value used if the column is not specified |
The following types can be auto mapped:
-
int
,long
,short
,float
,double
,BigDecimal
,BigInteger
: decimal separator can be.
or,
. Example10.05
-
boolean
: true/false -
string
: trim the string from the datatable -
enum
: the enum name can be used on datatable -
List
,Set
,Collection
: the generic can be a type managed by the mapping. Item on datatable are split using a,
-
OffsetDateTime
,LocalDateTime
,LocalDate
: date time. Can be ISO formatted date or relative date.Relative date can be specified using
now
keyword.You can add or subtract amount of time from now.
now + 1 day
,now - 3 weeks
.now
will return the same date at each step of the test.
If you want to use a custom type on datatable, you can write custom mapper. It can be useful when you want to convert id to object.
For example, If you have an object Customer
:
@DataTableWithHeader
record Customer(
@Column("code")
String code,
@Column("first name")
String firstName,
@Column("last name")
String lastName
) {
}
And you want map a datatable to the following bean:
@DataTableWithHeader
record Bean(
@Column("customer code")
Customer customer
) { }
If you want this steps to work
Given the following customer exists
| code | first name | last name |
| deblockt | Thomas | Deblock |
When I want to do something with a customer
| customer code |
| deblockt |
You need to write a function to map a string to a Customer. The function should be provided on your steps package.
@CustomDatatableFieldMapper(sample = "cucumberCode", typeDescription = "Customer")
public static Customer customerMapper(String customerCode) {
return TestContext.getCustomer(customerCode);
}
If you have big datatable, you can organize your objects using nested object.
For example, if you have a Customer
object, you can have a PersonalInformation
Object.
@DataTableWithHeader
record Customer(
@Column
String id,
@Column
PersonalInformation personalInformation
) {
}
@DataTableWithHeader
record PersonalInformation(
@Column
String firstName,
@Column
String lastName
) {
}
Using these objects, the datatable will look like
| id | first name | last name |
| 10 | Thomas | Deblock |
Now If you have another object Conversation
with two customer, like that:
@DataTableWithHeader
record Conversation(
@Column
Customer customer1,
@Column
Customer customer2
) {
}
If you write a datatable, you can not know if first name
column is the first name of the customer1
or
the customer2
.
To fix this issue, you can override the fields name using <parent_name>
, see this example:
@DataTableWithHeader
record Conversation(
@Column(value = "customer1", mandatory = false)
Customer customer1,
@Column(value = "customer2", mandatory = false)
Customer customer2
) {
}
// parent_name will be replaced by customer1 or customer2
@DataTableWithHeader
record Customer(
@Column("<parent_name> id")
String id,
@Column("<parent_name>")
PersonalInformation personalInformation
) {
}
// parent_name will be replaced by customer1 or customer2 coming from personalInformation annotation
@DataTableWithHeader
record PersonalInformation(
@Column("<parent_name> first name")
String firstName,
@Column("<parent_name> last name")
String lastName
) {
}
Using these objects, the following datatable will works:
# generate a Conversation object with only a customer1, and null for customer2
| customer1 id | customer1 first name | customer1 last name |
| 10 | Thomas | Deblock |
# generate a Conversation object with only a customer2, and null for customer1
| customer2 id | customer2 first name | customer2 last name |
| 10 | Thomas | Deblock |
# generate a Conversation object with a customer2, and a customer1
| customer2 id | customer2 first name | customer2 last name | customer2 id | customer2 first name | customer2 last name |
| 10 | Thomas | Deblock | 11 | Nicolas | Deblock |
When the column name is not explicitly specified using the @Column
annotation, the name is derived from the field name. Three naming strategies are available:
- Human Readable (default): Converts camel case field names to human-readable names by replacing camel case with spaces.
- Example:
myFieldName
becomesmy field name
.
- Example:
- Field Name: Keeps the original field name as the column name.
- Example:
myFieldName
becomesmyFieldName
.
- Example:
- Multi Name: Combines the previous two strategies, allowing either the original field name or the human-readable name.
- Example:
myFieldName
becomesmyFieldName
ormy field name
.
- Example:
See the configuration section to learn how to select a naming strategy.
By default, only fields with @Column
annotation are used as Datatable column.
If you don't want to need to add @Column
to all your fields, you can set the configuration cucumber.datatable.mapper.field-resolver-class
with the value com.deblock.cucumber.datatable.mapper.datatable.fieldresolvers.ImplicitFieldResolver
. In this case all fields of your class will be mapped as optional column.
With this configuration this class will be valid:
@DataTableWithHeader
class Bean {
public String firstColumn;
public String secondColumn;
}
If you want to ignore a field, you can use the @Ignore
annotation.
@DataTableWithHeader
class Bean {
public String firstColumn;
public String secondColumn;
@Ignore
public ComplexType fieldToIgnore;
}
You can configure some feature of this library to suit your coding preferences. The configuration work as the same way as default configuration
The following properties are available on cucumber.properties
, or using env var or system properties.
# The property cucumber.datatable.mapper.nameBuilderClass accepts these values:
# com.deblock.cucumber.datatable.mapper.name.HumanReadableColumnNameBuilder -- The field name is used and write using human-readable name. Example "first name"
# com.deblock.cucumber.datatable.mapper.name.UseFieldNameColumnNameBuilder -- The field name is used without any transformation. Example "firstName"
# com.deblock.cucumber.datatable.mapper.name.MultiNameColumnNameBuilder -- You can use human-readable name and fieldName.
cucumber.datatable.mapper.name-builder-class=com.deblock.cucumber.datatable.mapper.name.HumanReadableColumnNameBuilder
# The property cucumber.datatable.mapper.field-resolver-class accepts these values:
# com.deblock.cucumber.datatable.mapper.datatable.fieldresolvers.DeclarativeFieldResolver -- Only fields with @Column annotation will be mapped
# com.deblock.cucumber.datatable.mapper.datatable.fieldresolvers.ImplicitFieldResolver -- All fields of your class will be mapped
cucumber.datatable.mapper.field-resolver-class=com.deblock.cucumber.datatable.mapper.datatable.fieldresolvers.DeclarativeFieldResolver
If you need to build an executable fat jar to run your cucumber, you should add some configuration to work with this library.
This library use the java SPI definition to inject some class on cucumber runtime.
When you build a fat jar, this configuration can be override by cucumber definition, so we need to merge configuration files.
Using gradle you can use the shadow plugin to solve this issue. You can read this chapter to see how to merge configuration files.
example of task configuration:
plugins {
id 'com.github.johnrengelman.shadow' version '7.1.2'
}
shadowJar {
// specify jar name informations
archiveBaseName.set('cucumber-tests')
archiveClassifier.set('')
// add main and tests sources on jar
from sourceSets.main.output
from sourceSets.test.output
configurations = [
project.configurations.compileClasspath,
project.configurations.cucumberRuntime
]
// specify the cucumber Main class
manifest {
attributes "Main-Class": "io.cucumber.core.cli.Main"
}
// merge service files. Need to work with this lib.
mergeServiceFiles()
}
Using maven you can use the maven-shade-plugin to solve this issue. You can use this plugin configuration:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>