Reputation: 21081
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.
Upvotes: 1
Views: 154
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
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"
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();
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