Migrating to Cucumber-JVM 4.2
  August 28, 2019

Cucumber 2.0 and Java had some features that were very useful with step tables and custom parameters.

  • You could easily create a List<SomeClass> from a DataTable without any extra work.
  • The header names in the table were converted to Java camelCase, so that you have spaces in the header names. (e.g. header of “Extra Day Fee” was matched with attribute extraDayFee)
  • Custom types that had constructors taking a single String could be parameters to a step def (e.g. Dollar).

In order to get these features into 4.2, you need to add a TypeRegistryConfigurer.   The TypeRegistryConfigurer is described in the Cucumber documentation [here] and [here].  Other versions can be found on Grasshopper here and here.  This file needs to be located where the stepdef code is. The examples in those links and the code below use the Jackson Object Mapper during table processing  and parameter processing.

The addition in the code below is a convertMap method that changes the key names for maps to camelCase before passing them to the ObjectMapper.   Now names in feature files can look like they did in 2.0 (separated by spaces with any capitalization for the first letter of each word).

One you add the registration/transformer code from the end of the article, you can:

  • Create a List<SomeClass> from a DataTable without registration.  For example
//Step definition file
class SomeClass {
  int extraDayFee; 
  // more 
}

public class TableStepDefs {
  @Given("table is”) 
  public void dollar_is(List<SomeClass> aList) {
    // …
  }
}


// Feature file 
Given table is: 
  | extraDayFee |
  | 1           |
  • Use header names in the feature file that have spaces ((e.g. “Extra Day Fee”) to map to the extraDayFee attribute in the class which the DataTable has been converted to (e.g. SomeClass) .   For example this can be used with the previous SomeClass.
// Feature file 
Given table is: 
  | Extra Day Fee |
  | 1             |
  • Use custom types )with constructors taking a single String) as parameters to a step def without registration.  The corresponding match in the Cucumber expression is “{}”. For example:
import mystuff.Dollar; // Your custom type 

public class DollarStepDefs {
  @Given("dollar is {}")
  public void dollar_is(Dollar aDollarValue) {
    // …
  }
}

Using this TypeRegistryConfigurer provides the backward compatibility needed to allow you to use all your old feature files without having to change your glue code. In our example code, we chose to use the Jackson Object Mapper, but other mappers are available.

import cucumber.api.TypeRegistry;
import cucumber.api.TypeRegistryConfigurer;
import io.cucumber.cucumberexpressions.ParameterByTypeTransformer;
import io.cucumber.cucumberexpressions.ParameterType;
import io.cucumber.datatable.TableCellByTypeTransformer;
import io.cucumber.datatable.TableEntryByTypeTransformer;


import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static java.util.Locale.ENGLISH;

public class TypeRegistryConfiguration 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();  
    
    // Now it starts to work as before

    @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) {
      Map<String, String> convertedMap = convertMap(map);
      return objectMapper.convertValue(convertedMap, aClass);
    }

    @Override
    public <T> T transform(String s, Class<T> aClass) {
      return objectMapper.convertValue(s, aClass);
    }

    private Map<String, String> convertMap(Map<String, String> map) {
      Set<String> keys = map.keySet();
      Map<String, String> outputMap = new Hashtable<String, String>();
      for (String key : keys) {
        String objectValue = map.get(key);
        String newkey = convertKey(key);
        outputMap.put(newkey, objectValue);
      }
      return outputMap;
    }

    private String convertKey(String key) {
      return camelCase(key);
    }

    private String camelCase(String key) {
      StringBuilder output = new StringBuilder();
      boolean previousSpace = false;
      if (key.length() < 1)
        return "";
      output.append(Character.toLowerCase(key.charAt(0)));
      for (int i = 1; i < key.length(); i++) {
        char c = key.charAt(i);
        if (c != ' ') {
          if (previousSpace)
            output.append(Character.toUpperCase(c));
          else
            output.append(c);
          previousSpace = false;
        } else
          previousSpace = true;
      }
      return output.toString();
    }
  }
}