When we write a test using SerenityBDD, we have multiple ways to manage or initialize data. One of these ways is using data tables. But what happens when you send a data table from your feature file to use with your StepDefinitions? You get a parameter on your method as a DataTable, Map<String, String> even as a List<Map<String, String> making your code not very readable when you want to manipulate it.
In this article, I will explain Java records, a new feature from Java 16 that helps make the code within our step definitions more clean and streamlined.
First of all, we need to choose the Project language level: Using IntelliJ click on -> File -> Project Structure -> Project Option and Select 16 level to use Records, patterns, and local enums and interfaces.
Now, the implementation: Here is a feature file with a simple scenario and just a Given line using a data table.
Scenario: Add products to the cart
Given that Juan add products to the cart
| tennis | shirt |
| Adidas Running| Nike Sport L |
To work with this data table, I created a Java Record class within a models folder called ProductRecord as follows:
public record ProductRecord(String tennis, String shirt) {}
As you can see, this class receives the parameters that I am sending from the data table, but I do not need to create a constructor or getter and setter methods.
Now in the step definition class, I create a method that receives the features information to initialize it with the Record class. This method uses a tag annotation from Cucumber 6, that is @DataTableType. This annotation defines a converter method that transforms the data table into a Java Record class.
@DataTableTypepublicProductRecordselectProducts(Map<String, String> products){returnnewProductRecord(products.get("tennis"), products.get("shirt")); }
After converting the data table into a Record class, the step definition method that would receive the data table from the feature file now receives the Record class as a parameter.
@Given("^add products to the cart$")publicvoidaddProductsCart(ProductRecordproducts) {actor.attemptsTo(Select.theProducts(products),AddProducts.toCart() ); }
At this moment, I can work with the Record class easily in my Task class and call the data one by one.
publicclassSelectimplementsTask {privateProductRecordproducts;publicSelect(ProductRecordproducts){this.products = products; }@Overridepublic <TextendsActor> voidperformAs(Tactor) {actor.attemptsTo(Click.on(LBL_TYPE_PRODUCT.of(products.shirt())),Click.on(LBL_TYPE_PRODUCT.of(products.tennis())) ); }publicstaticSelecttheProducts(ProductRecordproducts){returninstrumented(Select.class, products); }}
That was an example of how this type of implementation can be used to work with data tables in the most simple way.
Some considerations:
If you declare the data table directly in your step definitions, then this implementation will help make your code more readable. If you are working with models or builders, then this implementation will help decrease the amount of code.
You can add static fields or static methods to a Record class and even create custom constructors. What you can not do is extend this class or make it abstract because this is a final class.
You should use Cucumber 6 to match the @DataTableType annotation with the Java Record object.
It is similar to Lombok, but in this case, it is included in Java. You can read about the Lombok library if you want to understand more about the differences.
Java Record is a new feature in Java 16 and onward. Please note that Java 16 is not a Long-Term-Support (LTS) release.
If you would like to see the implementation feel free to clone our repository in feature/java_record.
I hope that this example helps you work more effectively with data tables in your scripts.
References
A first look at records in java 14
Java 14 and IntelliJ IDEA
John Ferguson Smart Example — LinkedIn
Thanks to Joe Haubrich for his English editing corrections.
Comments