cucumbercucumber-jvmcucumber-java

Cucumber DataTableType annotation transposes table?


I have a small repo with the following dependencies: cucumber-java 7.16.1, junit-jupiter 5.10.2, selenium-java 4.21.0

I have 3 simple tests to check the saucedemo website where I'm creating a simple map to represent credentials data. In 2/3 cases the same map is generated, but when I'm using DataTableType, it looks like the item is transposed.

1. logs in using a datatable as a map:

Feature:

When I do a login with the following data as a map:
        | username | standard_user |
        | password | secret_sauce  |

Step definition:

public void loginAsMap(Map<String, String> credentials) 

Input parameter:

 credentials = {
    "username" -> "standard_user"
    "password" -> "secret_sauce"
    }

2. logs in using a datatable:

Feature:
 When I do a login with the following data as a simple DataTable:
        | username | standard_user |
        | password | secret_sauce  |

Step definition:

public void loginAsDataTableReversed(DataTable credentialsAsTable)

Input parameter:

credentialsAsTable = {
    raw = [ [ "username", "standard_user" ], [ "password", "secret_sauce" ] ]
    ...
}

As map:

credentialsAsTAble.asMap() =
    {
    "username" -> "standard_user"
    "password" -> "secret_sauce"
    }

3. logs in using a datatable converted to a SauceDemoCredentials object using DataTableType converter: (SauceDemoCredentials is a simple POJO with username / password)

Feature:

When I do a login with the following data as an object:
        | username | standard_user |
        | password | secret_sauce  |

Step definition:

public void loginAsObject(SauceDemoCredentials credentials) {

Converter:

@DataTableType
public SauceDemoCredentials credentials(Map<String, String> input) {
    //....
}

Input parameter:

    input = {
    "username" -> "password"
    "standard_user" -> "secret_sauce"
    }

As you can see, the same map is used in all 3 types to get an input. But the third test generates a transposed representation of map. Why is that? What am I missing here?


Solution

  • The problem with data tables is that a lot of the structure is implied but not explicit.

    For example this is a 3x4 table with the keys in the top:

    | firstName   | lastName | birthDate  |
    | Annie M. G. | Schmidt  | 1911-03-20 |
    | Roald       | Dahl     | 1916-09-13 |
    | Astrid      | Lindgren | 1907-11-14 |
    

    And this is a 3x4 table with the keys in the left column:

    | KMSY | 29.993333 |  -90.258056 |
    | KSFO | 37.618889 | -122.375000 |
    | KSEA | 47.448889 | -122.309444 |
    | KJFK | 40.639722 |  -73.778889 |
    

    So Cucumber always has to do some guess work.

    Now looking at your table:

    | username | standard_user |
    | password | secret_sauce  |
    

    When you ask Cucumber to transform this table into a map it guesses that the first column contains the keys and the second column contains the values. I can't really explain the why, but it seems to be common enough convention that got baked into Cucumber early on. People seem to like writing maps with with the keys on the first column.

    When using a @DataTableType you are using a different system and you are not asking Cucumber to turn the table into a map but rather a list of maps.

    Reusing the list of Authors from earlier:

    | firstName   | lastName | birthDate  |
    | Annie M. G. | Schmidt  | 1911-03-20 |
    | Roald       | Dahl     | 1916-09-13 |
    | Astrid      | Lindgren | 1907-11-14 |
    

    This table could be represented as List<Map<String, String>>. Or as a List<Author>. But to do that we need to define a data table type:

        @DataTableType
        public Author authorEntryTransformer(Map<String, String> entry) {
            return new Author(
                entry.get("firstName"),
                entry.get("lastName"),
                entry.get("birthDate"));
        }
    

    Cucumber here makes the implicit assumption that because we are dealing with a list of maps the headers will be in the first row. Hence the entries in the map are transposed. I can not explain why lists go down, but this too seems common enough.

    And for your example, while you did not ask for a List<SauceDemoCredentials>, Cucumber assumes you meant to use a list of exactly one item. A singleton.

    This was a design choice. Singletons could either be derived from a Map<String, List<String>> with a single value for each entry or from a List<Map<String, String>> with a single entry. Because a data table type that transforms a Map<String, String> is an entry transformer the latter made more sense to me. It keeps the terminology consistent (entries have the keys on the top). But expecting a single object to be derived from a map (keys on the side) also makes sense.

    Either-way, you can use the io.cucumber.java.Transpose annotation to tell Cucumber that the table should be transposed to create the SauceDemoCredentials.

    public void loginAsObject(@Transpose SauceDemoCredentials credentials) {
    

    And to explain the other possible transformers we have to reuse the list of airports from earlier:

    | KMSY | 29.993333 |  -90.258056 |
    | KSFO | 37.618889 | -122.375000 |
    | KSEA | 47.448889 | -122.309444 |
    | KJFK | 40.639722 |  -73.778889 |
    

    This table could be represented as a Map<String, List<BigInteger>>. Or as a Map<Airport, Coordinate> and for that we need to define two data table types:

        @DataTableType
        public Airport airportCellTransformer(String cell) {
            return new Airport(cell);
        }
    
        @DataTableType
        public Coordinate airportCellTransformer(List<BigInteger> values) {
            return new Coordinate(values.get(0), values.get(1));
        }
    

    For more information you can read: