How your 'Mother' can help you 'Build' cleaner unit tests - Part III
June 26, 2016We ended Part II with an inner
Builder for Address
, which forced us to revert to specifying seemingly arbitrary values in each
test that needed to build an Address
.
What I’ve usually seen at this point is pulling the Address objects into constants in each test
class. As the test suite grows, the number of private static final Address WHATEVER_ADDRESS = ...;
grows and grows. Doing that makes it hard to keep control over your test data, especially in a large
codebase. A minor change to Address, such as a new invariant or an added field, could cause
cascading changes to keep all those objects valid.
To avoid that, the next step is actually a very simple one, and boils down to centralizing the
definition and creation of an Address
with an Object Mother.
4) Introducing an Object Mother
An Object Mother is a type of Factory used to create example objects for testing. Let’s see it in action:
public class Address {
// ... no change, still using inner Builder
}
// Test only class
public class AddressMother {
public static Address.Builder address() {
return new Address.Builder()
.lineOne("123 Main St.")
.lineTwo("")
.city("Chicago")
.state("IL");
}
public static Address.Builder hawaiianAddress() {
return new Address.Builder()
.lineOne("123 Hawaii St.")
.lineTwo("")
.city("Honolulu")
.state("HI");
}
}
import static AddressMother.address;
public class ShippingServiceTest {
@Test
public void shipsToTheAddress() {
ShippingService service = new ShippingService();
Address shippingAddress = address().build();
Shipment shipment = service.shipTo(shippingAddress);
assertTrue(shipment.wasSuccessful());
assertEquals(shipment.getDeliveryAddress(), shippingAddress);
}
@Test
public void cannotShipToHawaii() {
ShippingService service = new ShippingService();
Shipment shipment = service.shipTo(hawaiianAddress().build());
assertFalse(shipment.wasSuccessful());
assertEquals(shipment.getFailureReason(), "Cannot ship to Hawaii.");
}
}
The Mother controls the values that constitute an example Address
. Now, when a test needs an
Address
it asks the Mother for one. Your Mother could return fully initialized Address
objects,
but instead we have chosen to have it return Builders. This powerful variation allows a test to get
a basic example object, and then modify it depending on what it is testing.
Give your Mother class the ability to build objects with various states as appropriate. In our case,
we currently have the need for a basic, valid address, and a Hawaiian address. But, be wary of
creating a different factory method for every example object you need. That may be a bit overkill.
I’d say to only create factory methods for things that represent core use cases and not one for each
edge case that you test. For example, if I wanted to test what happens when state
is empty, I’d do
address().state("").build()
over creating an addresWithoutAState()
factory method in the Mother.
The End
Whew, that’s it! Thanks for making it this far. If you skipped Parts
I or
II, I’d recommend going back to
see the entire journey. We teased out a nice pattern for managing test data using the Builder
pattern + Object Mothers. In doing so, we improved the design of our production code by making
Address
immutable and avoiding a sketchy constructor with a lot of parameters.