Published on

Testing Thymleaf with Java/Kotlin

Authors

Today I had to build and populate a Thymeleaf html template using a DTO populated in an upstream process. The fields in this DTO needed some processing done on them before they could be applied to the Thymeleaf context. A Thymeleaf context is basically a map where the key is the name of the variable in your template and the value is what that template variable will be replaced with in the template.

...
fun buildMyHtmlFile(someDto: SomeDto) {
    val context = Context()

    context.setVariable("myVar", someDto.myVar * 10)
...
}

One issue with the above approach is that it was tough testing that the variables hit the context and were prepared for it correctly (for example in the above we multiply by 10 before inserting myVar into the context). The Thymeleaf Context class is final so it is difficult and often impossible to mock / intercept using most testing frameworks.

The solution I used was pulling out the variable manipulation logic into a translator for each DTO that was to be placed into a Thymeleaf context. We refer to target objects that will be used in some sort of front-end rendering be it a webpage, pdf or email as ViewModels or VMs for short. Using this translator approach made it trivial to have targeted tests on the translator to confirm that we are preparing variables for the context correctly.

interface Translator<I, O>{
    O translate(I objectToTranslate);
}
fun buildMyHtmlFile(someDto: SomeDto) {
    val context = Context()
    val someVM = SomeContextVMTranslater.translate(someDto)
    context.setVariable("myVar", someVM.myVar)
    ...
}

The other difficulty with testing Thymeleaf is even if you have the calculation logic tested you still cannot confirm that the variables are being plugged into the context correctly i.e. context.setVariable("myVar", someVM.myVar). The solution to this was to extend the translator interface a bit and require a method to be implemented that returns a map of context variable names to context object values.

interface Translator<I, O>{
    O translate(I objectToTranslate);

    Map<String, Object> thymeleafContextVariableToValueMap();
}
fun buildMyHtmlFile(someDto: SomeDto) {
    val context = Context()
    val someVM = SomeContextVMTranslator.translate(someDto)
    val thymeLeafContextMap = SomeContextVMTranslator.thymeleafContextVariableToValueMap().forEach{ (thymeLeafVar, value) ->
        context.setVariable(thymeLeafVar, value)
    }
    ...
}

The last problem to solve with the approach up until now is that the forEach is very boilerplate and can definitely be handled by a common method. I opted to use Java 8 to define the Translator interface as I can leverage the ability to define default implementations in interfaces which is not yet possible in Kotlin interfaces.

interface Translator<I, O>{
    O translate(I objectToTranslate);

    Map<String, Object> thymeleafContextVariableToValueMap();

    default void translateToThymeleafContext(Context context, I input){
            O translatedVariable = translate(input);
            Map<String,Object> contextMap = thymeleafContextVariableToValueMap(translatedVariable);
            contextMap.forEach((thymeLeafVar, value) -> {
                context.setVariable(thymeLeafVar, value);
            });
    }
}
fun buildMyHtmlFile(someDto: SomeDto) {
    val context = Context()
    SomeContextVMTranslator.translateToThymeleafContext(context,someDto)
    ...
}