When testing Data Transfer Objects (DTOs) or Transfer Objects you can go a few different routes. If you’re a purist you would say that a unit test should only be written for code that matters. I agree completely with this but when running coverage reports, having code that says it’s 50% covered by tests is a concerning. You then have to dig through the report to see if you actually have good coverage or not. Writing tests which do nothing that test getters and setter are no fun and seem like a waste of time. There are way more interesting problems to solve out there! During some downtime I decided to solve this problem since I got sick of seeing code coverage that wasn’t 90+% when I knew we were at that target if we ignored the DTOs and transfer objects. I decided to spend an hour or two to solve this problem once and for all and came up with a pretty simple solution: Create an abstract test class which does this for me.
When creating this I knew I needed to make this as simple as possible. I needed a way for primitives and common objects to be automatically created. I needed to create the right object for the field/setter, call the setter method with the object, and then verify that the same object is returned when calling the getter. I also needed a way for a user to specify how to create an object if a no-arg constructor isn’t available for non-primitive objects. I created some common creators with the following code and also allow for test classes which extend the class to send in their own custom Supplies or factory methods as well.
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.function.Supplier;
import org.junit.Test;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
/**
* A utility class which allows for testing entity and transfer object classes. This is mainly for code coverage since
* these types of objects are normally nothing more than getters and setters. If any logic exists in the method, then
* the get method name should be sent in as an ignored field and a custom test function should be written.
*
* @param <T> The object type to test.
*/
public abstract class DtoTest<T> {
/** A map of default mappers for common objects. */
private static final ImmutableMap<Class<?>, Supplier<?>> DEFAULT_MAPPERS;
static {
final Builder<Class<?>, Supplier<?>> mapperBuilder = ImmutableMap.builder();
/* Primitives */
mapperBuilder.put(int.class, () -> 0);
mapperBuilder.put(double.class, () -> 0.0d);
mapperBuilder.put(float.class, () -> 0.0f);
mapperBuilder.put(long.class, () -> 0l);
mapperBuilder.put(boolean.class, () -> true);
mapperBuilder.put(short.class, () -> (short) 0);
mapperBuilder.put(byte.class, () -> (byte) 0);
mapperBuilder.put(char.class, () -> (char) 0);
mapperBuilder.put(Integer.class, () -> Integer.valueOf(0));
mapperBuilder.put(Double.class, () -> Double.valueOf(0.0));
mapperBuilder.put(Float.class, () -> Float.valueOf(0.0f));
mapperBuilder.put(Long.class, () -> Long.valueOf(0));
mapperBuilder.put(Boolean.class, () -> Boolean.TRUE);
mapperBuilder.put(Short.class, () -> Short.valueOf((short) 0));
mapperBuilder.put(Byte.class, () -> Byte.valueOf((byte) 0));
mapperBuilder.put(Character.class, () -> Character.valueOf((char) 0));
mapperBuilder.put(BigDecimal.class, () -> BigDecimal.ONE);
mapperBuilder.put(Date.class, () -> new Date());
/* Collection Types. */
mapperBuilder.put(Set.class, () -> Collections.emptySet());
mapperBuilder.put(SortedSet.class, () -> Collections.emptySortedSet());
mapperBuilder.put(List.class, () -> Collections.emptyList());
mapperBuilder.put(Map.class, () -> Collections.emptyMap());
mapperBuilder.put(SortedMap.class, () -> Collections.emptySortedMap());
DEFAULT_MAPPERS = mapperBuilder.build();
}
/** The get fields to ignore and not try to test. */
private final Set<String> ignoredGetFields;
/**
* A custom mapper. Normally used when the test class has abstract objects.
*/
private final ImmutableMap<Class<?>, Supplier<?>> mappers;
/**
* Creates an instance of {@link DtoTest} with the default ignore fields.
*
* @param customMappers Any custom mappers for a given class type.
* @param ignoreFields The getters which should be ignored (e.g., "getId" or "isActive").
*/
protected DtoTest(Map<Class<?>, Supplier<?>> customMappers, Set<String> ignoreFields) {
this.ignoredGetFields = new HashSet<>();
if (ignoreFields != null) {
this.ignoredGetFields.addAll(ignoreFields);
}
this.ignoredGetFields.add("getClass");
if (customMappers == null) {
this.mappers = DEFAULT_MAPPERS;
} else {
final Builder<Class<?>, Supplier<?>> builder = ImmutableMap.builder();
builder.putAll(customMappers);
builder.putAll(DEFAULT_MAPPERS);
this.mappers = builder.build();
}
}
/**
* Returns an instance to use to test the get and set methods.
*
* @return An instance to use to test the get and set methods.
*/
protected abstract T getInstance();
}
As you can tell, I’m using some Java 8 magic for Suppliers here. If I wanted to be more creative, I could use random number generator each time we create an object or even cache the objects as static variables. I’m keeping it simple for this post though. This base test class now has a good set of default mappers and also allows for test classes which extend it to send in custom mappers, if the class that’s being tested is an interface, abstract object, or an object which doesn’t have a no-arg constructor. I also added a some functionality to allow the test class to ignore selected get fields. Some fields may be marked transient but another field is used in its place and performs logic. An example of this could be to ignore a date and time fiend but having a custom getDateTime() function which combines them together but there is no setter. In this case we’d want to send in “getDateTime” as an ignore field and write a manual JUnit test for the method since it’s actually test worthy now.
Now that I have my Suppliers, I can create objects of whatever type with the following method:
/**
* Creates an object for the given {@link Class}.
*
* @param fieldName The name of the field.
* @param clazz The {@link Class} type to create.
*
* @return A new instance for the given {@link Class}.
*
* @throws InstantiationException If this Class represents an abstract class, an interface, an array class, a
* primitive type, or void; or if the class has no nullary constructor; or if the instantiation fails
* for some other reason.
* @throws IllegalAccessException If the class or its nullary constructor is not accessible.
*
*/
private Object createObject(String fieldName, Class<?> clazz)
throws InstantiationException, IllegalAccessException {
final Supplier<?> supplier = this.mappers.get(clazz);
if (supplier != null) {
return supplier.get();
}
if (clazz.isEnum()) {
return clazz.getEnumConstants()[0];
}
try {
return clazz.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException("Unable to create objects for field '" + fieldName + "'.", e);
}
}
So now we have a way to create objects but we need to actually test them. The tricky part here is that we need to use reflection to match the getter and setter together. An issue arises when an object is marked as @Immutable. Normally in this case you’ll have get methods by no setters. This is a valid use case for Hibernate objects where your application shouldn’t be changing anything and Hibernate will set the field via reflection. Not having setters helps reinforce the idea that the object is not mutable so engineers don’t waste time writing code that won’t work. In this case, I use reflection to set the field and then call the getter for code coverage. The following code actually maps everything together via a GetterSetterPair object (don’t worry, a link to the full source is at the bottom of post):
/**
* Tests all the getters and setters. Verifies that when a set method is called, that the get method returns the
* same thing. This will also use reflection to set the field if no setter exists (mainly used for user immutable
* entities but Hibernate normally populates).
*
* @throws Exception If an unexpected error occurs.
*/
@Test
public void testGettersAndSetters() throws Exception {
/* Sort items for consistent test runs. */
final SortedMap<String, GetterSetterPair> getterSetterMapping = new TreeMap<>();
final T instance = getInstance();
for (final Method method : instance.getClass().getMethods()) {
final String methodName = method.getName();
if (this.ignoredGetFields.contains(methodName)) {
continue;
}
String objectName;
if (methodName.startsWith("get") && method.getParameters().length == 0) {
/* Found the get method. */
objectName = methodName.substring("get".length());
GetterSetterPair getterSettingPair = getterSetterMapping.get(objectName);
if (getterSettingPair == null) {
getterSettingPair = new GetterSetterPair();
getterSetterMapping.put(objectName, getterSettingPair);
}
getterSettingPair.setGetter(method);
} else if (methodName.startsWith("set") && method.getParameters().length == 1) {
/* Found the set method. */
objectName = methodName.substring("set".length());
GetterSetterPair getterSettingPair = getterSetterMapping.get(objectName);
if (getterSettingPair == null) {
getterSettingPair = new GetterSetterPair();
getterSetterMapping.put(objectName, getterSettingPair);
}
getterSettingPair.setSetter(method);
} else if (methodName.startsWith("is") && method.getParameters().length == 0) {
/* Found the is method, which really is a get method. */
objectName = methodName.substring("is".length());
GetterSetterPair getterSettingPair = getterSetterMapping.get(objectName);
if (getterSettingPair == null) {
getterSettingPair = new GetterSetterPair();
getterSetterMapping.put(objectName, getterSettingPair);
}
getterSettingPair.setGetter(method);
}
}
/*
* Found all our mappings. Now call the getter and setter or set the field via reflection and call the getter
* it doesn't have a setter.
*/
for (final Entry<String, GetterSetterPair> entry : getterSetterMapping.entrySet()) {
final GetterSetterPair pair = entry.getValue();
final String objectName = entry.getKey();
final String fieldName = objectName.substring(0, 1).toLowerCase() + objectName.substring(1);
if (pair.hasGetterAndSetter()) {
/* Create an object. */
final Class<?> parameterType = pair.getSetter().getParameterTypes()[0];
final Object newObject = createObject(fieldName, parameterType);
pair.getSetter().invoke(instance, newObject);
callGetter(fieldName, pair.getGetter(), instance, newObject);
} else if (pair.getGetter() != null) {
/*
* Object is immutable (no setter but Hibernate or something else sets it via reflection). Use
* reflection to set object and verify that same object is returned when calling the getter.
*/
final Object newObject = createObject(fieldName, pair.getGetter().getReturnType());
final Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(instance, newObject);
callGetter(fieldName, pair.getGetter(), instance, newObject);
}
}
}
/**
* Calls a getter and verifies the result is what is expected.
*
* @param fieldName The field name (used for error messages).
* @param getter The get {@link Method}.
* @param instance The test instance.
* @param fieldType The type of the return type.
* @param expected The expected result.
*
* @throws IllegalAccessException
* @throws IllegalArgumentException
* @throws InvocationTargetException
*/
private void callGetter(String fieldName, Method getter, T instance, Object expected)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
final Object getResult = getter.invoke(instance);
if (getter.getReturnType().isPrimitive()) {
/* Calling assetEquals() here due to autoboxing of primitive to object type. */
assertEquals(fieldName + " is different", expected, getResult);
} else {
/* This is a normal object. The object passed in should be the exactly same object we get back. */
assertSame(fieldName + " is different", expected, getResult);
}
}
Ahh finally, we see some actual test methods! This method is the only method annotated with a @Test annotation and then we call callGetter() which will actual verify that the object we passed in the same object as the one we set. Why assertSame() instead of assertEquals()? Well we want to make sure the created object is the exact same object we set before and that no shenanigans happened in the setter or getter method. Again, if these method do something special then a normal JUnit test should be written and the getter name should be sent in as an ignore field. Now we just need to test our file:
/**
* Tests the {@link EverythingTransfer} class.
*/
public class EverythingTransferTest extends DtoTest<EverythingTransfer> {
@Override
protected EverythingTransfer getInstance() {
return new EverythingTransfer();
}
}
So we now turned what would have been many boring unit tests which didn’t test any real business logic into a simple file with less than 10 lines of code. All we need to do is extend DtoTest and create a test instance and the DtoTest file will do the rest.
Hopefully this post helps. Although it’s heavy reflection based and may be hard to follow you can run the code as-is and get 100% code coverage just by creating a test class for your DTO and implementing the createInstance() method. Obviously if you have any additional methods (equals(), toString,(), etc), you’ll need to write units tests for those method manually to get 100% coverage for your class. Full code and a test project can be viewed here.
Thanks for reading and hopefully this post helps improve code coverage and maybe save a few poor programmers who are forced to write to manual tests for such simple things.