My last post covered the use of matchers with an example of code which maps from one data structure to another. In general, testing mapping code is pretty tedious. Every time a corresponding pair of fields is added to data structures, a new test is needed to verify that the fields are mapped properly. Depending on the specific case, more tests may be necessary to cover the cases where the value is not present one or the other data structure.

The addition of lambda expressions in Java 8 raises new possibilities for reducing this pain. In this post I'll show how to test the mapping in such a way that each new field introduces a minimum of additional bootstrap, while retaining all of the advantages of clear, focused tests with matchers.

Let's first review the mapping problem from the last post. We start with some classes which are to be mapped.

class Address {
    private String houseNumber;
    private String street;
    private String postalCode;
    private String city;
    private String country;
    private String phone;

    // Getters for the above fields
}

class Customer {
    private String name;
    private Address address;
    private CustomerType type;
}

enum CustomerType {
    PRIVATE_CUSTOMER, BUSINESS_CUSTOMER, UNKNOWN
}

class ExternalCustomer {
    private ExternalAddress address;
    private ExternalCustomerType type;
}

class ExternalAddress {
    private String name;
    private String addressLine1;
    private String addressLine2;
    private String zip;
    private String city;
    private String state;
    private String country;
    private String phone;

    // Getters and setters for the above fields
}

enum ExternalCustomerType {
    PRIV, BUS, UNKNOWN
}

The method under test has the following signature.

List<ExternalCustomer> convertCustomers(List<Customer> customers)

How do we handle the many fields? Three options come to mind:

  • Write (at least) one test per field. This involves a lot of overhead per field.
  • Write one test for several fields and include several separate asserts in it. This has the disadvantage that only the first assertion which fails will result in a message, possibly hiding the scope of the problem in case of failure.
  • Write one test for several fields and package all asserts together. If there are, however, many fields, only one of which is incorrect, the expected output will nevertheless normally include all fields, making it hard to find the correct one.

A parametrised test

I present here an alternative approach using a single parametrised test. For this, we need an addition to JUnit to support parametrised tests at the method level. I find JUnitParams quite good and will use it here. This requires first binding the JUnitParams library into the project and then setting the test runner appropriately on the test class.

@RunWith(JUnitParamsRunner.class)
class ConverterTest {
    ...
}

We shall define a class Mapper which will serve as the single parameter. It contains roughly a description of the test case, instructions for constructing the input, and a matcher to be applied to the output. Here I shall assume that we use a builder idiom to construct the customer object.

public class CustomerMapping {
    private final String description;
    public final Function<Customer.Builder, Customer.Builder>
        inputTransform;
    public final Matcher matcher;

    // Constructor to set the fields directly from its params
    public CustomerMapping(...) { ... } 

    @Override
    public String toString() {
        return description;
    }
}

The implementation of toString ensures that the JUnit report lists the description along with the test case.

How is the test implemented? Easy:

@Test
@Parameters(method = "parametersForConversion")
public convertShouldConvertData(CustomerMapping mapping) {
    Customer customer =
        mapping.inputTransform.apply(Customer.builder()).build();    assertThat(convertCustomers(ImmutableList.of(customer)),
        contains(mapping.matcher));
}

This creates some object with default settings and applies the transformation for this test case to it. It then sends the result to the system under test and checks that the matcher for this test case matches the result.

How do the parameters appear? First, we recall the PropertyMatcher from the last post:

public class PropertyMatcher<T, U> {
    private final String propertyName;
    private final Function<T, U> accessor;

    public static <T, U> PropertMatcher<T, U>
         property(String propertyName, Function<T, U> accessor) {
        return new PropertyMatcher<>(propertyName, accessor);
    }

    public PropertyMatcher(String propertyName,
            Function<T, U> accessor) {
        this.propertyName = propertyName;
        this.accessor = accessor;
    }

    public FeatureMatcher<T, U> matches(
            Matcher<? super U> innerMatcher) {
        return new FeatureMatcher<>(innerMatcher, propertyName,
                propertyName) {
            @Override
            public U propertyValueOf(T actual) {
                return accessor.apply(actual);
            }
        }
    }

    public FeatureMatcher<T, U> is(U value) {
        return matches(is(value));
    }
}

Here I've added an additional method "is" to assist in readability. Now we just have one case per field.

public static Object[] parametersForConversion() {
    return new Object[] {
        new CustomerMapping("name",
            (c) -> c.withName("Max Mustermann"),
            property("name", (c) -> c.getAddress().getName())
                .is("Max Mustermann")),

        new CustomerMapping("address line 1",
            (c) -> 
                c.withStreet("Hauptstraße")
                 .withHouseNumber("62"),
            property("address line 1",
                    (c) -> c.getAddress().getAddressLine1())
                .is("Hauptstraße 62")),

        new CustomerMapping("postal code",
            (c) -> c.withPostalCode("12345"),
            property("zip", (c) -> c.getAddress().getZip())
                .is("12345")),
        ...
    };
}

Now each field gets its own test case, showing up separately in the report tagged with the description passed as the first argument to CustomerMapping.

The class Mapping can also be made generic with respect to the sources and targets of the mapping. Then the effort of creating the associated classes need only be made once.

Category:  Walkthroughs  |  Tags:  Java   software development   testing