Using Reflection to Test Complex Objects

"Looking back" by Susanne Nilsson, licensed under CC BY-SA 2.0
“Looking back” by Susanne Nilsson, licensed under CC BY-SA 2.0

Having code with automated tests keeps our quality high and makes us more efficient. But some code can challenge that efficiency—when code is simple in function yet complex in structure, making minor structure changes can be problematic. We don’t always completely think through the impact of those changes, sometimes accidentally leaving pieces of functionality untested and creating bugs. Thankfully, there’s a way to reclaim that efficiency and maximize our test coverage.

Dealing with Complexity, Automatically

Reflection, or introspection, is a technique available in many languages by which you inspect an object’s structure at runtime. It’s often used in library code to work with objects whose structures are unknown. As it turns out, using reflection is also a useful way to inspect the structure of a complex object with a lot of properties that share a common behavior, so we can test that behavior efficiently for each property in turn.

As an example, let’s take a large immutable data object implemented in Java. It’s got a lot of private fields, with a getter for each field. When we want to change one of these data objects in our code, we use a builder that reads all the fields from the original, accepts changed values via its own methods, then builds a new data object with the original values as defaults and the specified values changed.

We want to test the copy behavior of this object and its builder. This test is critically important because, if we miss copying a field, the new data object won’t carry the value forward from the original—creating a potentially serious bug.

What’s in a class?

We’ll use Java reflection on our object under test, discovering all its properties, then write one test that covers all the properties that behave the same way, running that test against each one in turn:

public class EmployeeTest {
 
    @Test
    public void copiesAllFields() throws Exception {
 
        Employee original = new Employee();
        populateFields(original);
        Employee copy = new EmployeeBuilder(original).build();
 
        for (Field field : getAllFields(original.getClass())) {
            field.setAccessible(true);
            if (!field.get(original).equals(field.get(copy))) {
                throw new AssertionError("original=" + field.get(original) +
                                         ", copy=" + field.get(copy) +
                                         " (" + field.getName() + ")");
            }
        }
 
    }
 
}

To make this test work, the first thing we have to do is build a list of Fields that we can iterate over. There are two specific challenges we run into here with Java reflection:

  1. our fields are all private (which is also why we need to call setAccessible on them)
  2. our data object class inherits from other classes with their own private fields

To solve these problems, we’ll need to walk up the class hierarchy to build our complete list of fields (for efficient array concatenation, we’re using Guava ObjectArrays):

private Field[] getAllFields(Class c) {
 
    Field[] fields = c.getDeclaredFields();
    Class superclass = c.getSuperclass();
    if (superclass == Object.class) {
        return fields;
    }
    return ObjectArrays.concat(fields,
                               getAllFields(c.getSuperclass()),
                               Field.class);
 
}

Populating the Test Object

Now that we have a complete list of fields, the assertion part of our test is done. The last thing we have to do is implement populateFields, a method that walks over our original object and puts a unique value in each field, allowing us to assert both that the field was copied and that field values aren’t accidentally duplicated to the wrong place.

This method will expand pretty quickly, as it will need to be able to populate unique values for many different types of fields. But here’s a simple illustration for fields that are either ints or objects that can be instantiated with a no-arg constructor. We assume the existence of a uniqueValue method that returns a unique value each time it’s called—this insures that we’re always populating fields uniquely.

private void populateFields(T destination)
        throws IllegalAccessException, InstantiationException {
 
    for (Field field : getAllFields(destination.getClass())) {
        Class type = field.getType();
        if (type == Integer.class) {
            field.set(destination, Integer.valueOf(uniqueValue()));
        }
        else {
            field.set(destination, type.newInstance());
        }
    }
 
}

That’s it. Now you have automatic test coverage any time you add a field to Employee. Being a good test-driven developer, of course, you’ll want to see this in a red-green-refactor cycle—so go ahead and add a field with a getter to Employee without implementing its copy in the builder. The test will pick up on this new field and fail since it wasn’t copied properly, and you’ll be ready to make it pass by implementing the missing behavior.

You can (and should!) expand your use of this test pattern to cover other discoverable behaviors of objects you create that conform to patterns—for example, testing the getters on your data object, or testing that the methods on the builders change the values on the copied object and not the original. You need only write the code to do the reflection and one test for each behavior; from then on, you’ll gain failing tests effectively for free whenever you make a change to these data objects—scaling up to a lot of value for a relatively small investment.