Emil L
Emil L

Reputation: 21081

Dealing with code duplication in tests

At work I practice Test Driven Development as much as possible. One thing I often end up in though is having to set up a bunch of DTO's and when these have a slightly complex structure this becomes quite alot of code. The problem with this is that the code is often quite repettetive and I feel it distracts from the main purpose of the test. For instance using a slightly contrieved and condensed example (in java, jUnit + mockito):

class BookingServiceTest {

    private final static int HOUR_IN_MILLIS = 60 * 60 * 1000;
    private final static int LOCATION_ID = 1;
    @Mock
    private BookingLocationDao bookingLocationDao;

    @InjectMocks
    private BookingService service = new BookingService();

    @Test
    public void yieldsAWarningWhenABookingOverlapsAnotherInTheSameLocation() {
        // This part is a bit repetetive over many tests:
        Date now = new Date()
        Location location = new Location()
        location.setId(LOCATION_ID);

        Booking existingBooking = new Booking()
        existingBooking.setStart(now);
        existingBooking.setDuration(HOUR_IN_MILLIS);
        existingBooking.setLocation(location);
        // To here

        when(bookingLocationDao.findBookingsAtLocation(LOCATION_ID))
            .thenReturn(Arrays.asList(existingBooking));

        // Then again setting up a booking :\
        Booking newBooking = new Booking();
        newBooking.setStart(now);
        newBooking.setDuration(HOUR_IN_MILLIS / 2);
        newBooking.setLocation(location);               


        // Actual test...
        BookingResult result = service.book(newBooking);

        assertThat(result.getWarnings(), hasSize(1));
        assertThat(result.getWarnings().get(0).getType(), is(BookingWarningType.OVERLAPING_BOOKING));
    }

}

In this example the setup is not that complicated so I wouldn't think of it too much. However, when more complicated input is required the code for the setup of the input to methods tend to grow. The problem gets exacerbated by similar input being used in several tests. Refactoring the setup code into a separate TestUtil class helps a bit. The problem then is that it is a bit hard to find these utility classes when writing new tests a couple of month's later, which then leads to duplication.

  1. What is a good way of dealing with this kind of "complex" DTOs in order to minimize code duplication in test setups?
  2. How do you ensure that extracted TestUtilities are found when working with similar code?
  3. Am I doing it wrong? :) Should I build my software in another way as to avoid this situation altogether? If so, how?

Upvotes: 1

Views: 154

Answers (2)

gmn
gmn

Reputation: 4319

As Erik rightly points out the frequently used patterns to solve this are TestDataBuilder and ObjectMother. These are also covered in depth in: Mark Seemans advanced unit testing course as well as growing object oriented software guided by tests, both are very good.

In practice I find that the Test Data Builder pattern almost always leads to better, more readable tests than the ObjectMother pattern except in the simplest cases (as you often need a surprising number of overloads for the objectmother pattern).

Another trick that you can use is to bunch together sets of setups with the test object builder pattern into single methods e.g.

Invoice invoiceWithNoPostcode = new InvoiceBuilder()
    .withRecipient(new RecipientBuilder()
        .withAddress(new AddressBuilder()
            .withNoPostcode()
            .build())
        .build())
    .build();

Could become:

new InvoiceBuilder().WithNoPostCode().Build();

And in some cases that can lead to even simpler test setups, but doesn't work in all cases.

Upvotes: 2

Erik Öjebo
Erik Öjebo

Reputation: 10851

There are a couple of patterns which are of interest for handling this type of situation:

For an in-depth discussion about these patterns, take a look at the excellent book "Growing Object-oriented Software Guided by Tests"

Test Data Builder

A builder class is created for each class for which you want to facilitate instantiation/setup. This class contains a bunch of methods which set up the object being built in specific states. Usually these helper methods return an instance to the builder class, so that the calls can be chained in a fluent style.

// Example of a builder class:
public class InvoiceBuilder {
    Recipient recipient = new RecipientBuilder().build();
    InvoiceLines lines = new InvoiceLines(new InvoiceLineBuilder().build());
    PoundsShillingsPence discount = PoundsShillingsPence.ZERO;

    public InvoiceBuilder withRecipient(Recipient recipient) {
        this.recipient = recipient;
        return this;
    }

    public InvoiceBuilder withInvoiceLines(InvoiceLines lines) {
        this.lines = lines;
        return this;
    }

    public InvoiceBuilder withDiscount(PoundsShillingsPence discount) {
        this.discount = discount;
        return this;
    }

    public Invoice build() {
        return new Invoice(recipient, lines, discount);
    }
}

// Usage:
Invoice invoiceWithNoPostcode = new InvoiceBuilder()
    .withRecipient(new RecipientBuilder()
        .withAddress(new AddressBuilder()
            .withNoPostcode()
            .build())
        .build())
    .build();

Object Mother

An object mother is a class that provides pre-made test data for different common scenarios.

Invoice invoice = TestInvoices.newDeerstalkerAndCapeInvoice();

The examples above are borrowed from Nat Pryce's blog.

Upvotes: 3

Related Questions