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
name,target_bean_name
description,null
tasks[1],target_task2
nestedObjects[0].tasks[1],nestedTask11
- The first column is expression for BeanUtil to extract value from the bean field.
- The second column is expected value. I have a special case where a field is
null
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'