Skip to content

Use Jodd BeanUtil for testing data mapping

Posted on:March 20, 2023 at 02:36 PM

Introduction

Recently, I have a task to verify lengthy mapping input data into a java object (pojo). The mapping logic is not something science rocket but making the library indestructible is quite a boring process. Considering that the pojo class has so many pojo classes as fields and can dig down up to 6-7 layer and each layer has about 40-70 fields.

So the test we write to cover unit test can be something like the following code:


@Test
public class MappingTest {
    public void simpleTest() {
        var bean = new Bean();
        bean = SomeMagicEntryPoint.getFromInput(input);
        assertEquals(bean.getL1().getL2().getL3().get(0).getL4(), someValue);
        // repeat this assert kind of thing for rest of the bean fields
        assertEquals(bean.getL1().getL2().getL3().get(0).getField60(), someValue);
    }
}

Rather than repeat myself such kind of boring getter and assert call, I need to jump to a tool to get java field value from expression. This can be easily done by an interchangeable format such as json where I construct json string from output of the mapping function then comparing it with prepared expected json string. Another possibility is that I construct json string from the bean then using jsonPath to extract and validate expected values. I can draft this plan as following steps:

- Prepare intermediate representation of the bean
- Use expected value check with step 1 value as a whole
- Alternatively, use expression to get value from the intermediate representation then validate value
- For each test, prepare a set of pair[expression, expected value] to feed into test

The fun part of this naive approach in my mind is: should we use json / xml where jsonPath and xPath work really well? After a few searchs, I found that Java has some library to get bean field value by expression. One of them is Jodd BeanUtil which I use for my example below. It follows exactly above plan I draft for a tests:

- It has externalized expression and expected value from a csv file
- For a test, I only have to call a loop to evaluate all expression I want to test

You can have a reference at: Travisnv/sample-jodd-bean-utils

Sample csv file

name,target_bean_name
description,null
tasks[1],target_task2
nestedObjects[0].tasks[1],nestedTask11

By doing this approach, I am free from intermediate format such as json/xml and all test classes are now spotless and easy to follow.By observing a common assert method and externalized files, I can wrap them up into hundreds of tests easily. In my example, I have stripped out all blows and whistle classes of an actual working test framework, I leave bare minimum class to demonstrate my proposed flow. In a real project, there are only a few methods calls per test.

Sample test

Here is a quick and naive test here:

import jodd.bean.BeanUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.travisnv.jodd.NestedObject;
import org.travisnv.jodd.TargetBean;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class JoddTest {

    TargetBean bean;

    @BeforeEach
    void beforeTest() {
        //build object simulating mapping object.
        var nested = new NestedObject();
        nested.setNestedField("valueFromNested1");
        nested.setNestedPrimitive(1);
        nested.setTasks(List.of("nestedTask1", "nestedTask11"));

        var nested2 = new NestedObject();
        nested2.setNestedField("valueFromNested2");
        nested2.setNestedPrimitive(2);
        nested2.setTasks(List.of("nestedTask2", "nestedTask22"));

        var bean = new TargetBean();
        bean.setName("target_bean_name");
        bean.setTasks(List.of("target_task1", "target_task2"));
        bean.setNestedObjects(List.of(nested, nested2));
        this.bean = bean;
    }

    @Test
    void test() {
        System.out.println("Start test....");
        System.out.println(bean);
        // read csv file for validate all expressions along with expected data
        try (var ins = getClass().getResourceAsStream("test1_expression.csv")) {
            var expressions = new String(ins.readAllBytes(), Charset.defaultCharset());
            Arrays.stream(expressions.split(System.lineSeparator()))
                    .map(x -> x.split(","))
                    .forEach(test -> testExpression(test[0], test[1], bean));
        } catch (IOException e) {
            System.out.println("There is error for reading expression file. Please check again.");
            throw new RuntimeException(e);
        }
        System.out.println("Test completed!");
    }

    // a common method to validate field value, an if statement for handle null scenario
    private void testExpression(String expression, String expected, Object bean) {
        System.out.println("Test bean with exp:`" + expression + "` to have value:`" + expected + "`");
        if (expected.equals("null")) {
            assertTrue(Optional.ofNullable(BeanUtil.pojo.getProperty(bean, expression)).isEmpty());
        } else {
            assertEquals(BeanUtil.pojo.getProperty(bean, expression), expected);
        }
    }
}

Installation

Maven:


<dependency>
    <groupId>org.jodd</groupId>
    <artifactId>jodd-util</artifactId>
    <version>5.1.6</version>
</dependency>

Gradle:

implementation 'org.jodd:jodd-util:5.1.6'