Testing with factories
On July 1st, Jelmer added a very useful blog post about testing the database layer in which he suggested to use “insert statement” and “fixture” classes to provide a good way to insert test data into your database. I am also using that technique as I’m writing unit tests for the database layer. I have to say this really makes unit tests less work to write, more focussed and therefore more fun to write!
Although this technique solves the challenges we have in database layer tests, we might encounter similar challenges when writing unit tests for other layers (such as the service or frontend layer). This is because such tests often also require “test data” you need to create. Only this time the test data is not in the form of database rows, but in the form of Java objects. Such objects need to be instantiated in a valid state and often depend on other objects. I find this quite similar to inserting rows into database tables having not null columns and/or foreign key constraints.
The challenge
As an example I made up the following unit test (using JUnit):
@Test public void testCalculateTotalOrderPrice() { Address address = new Address("Some Street", "13a", "1234AB", "City"); User owner = new User("Firstname", "Lastname", address); Order order1 = new Order(Status.SENT, owner, new BigDecimal("15")); Order order2 = new Order(Status.SENT, owner, new BigDecimal("35")); BigDecimal totalPrice = service.calculateTotalOrderPrice(Arrays.asList(order1, order2)); assertEquals(new BigDecimal("50"), totalPrice); }
Here we test that the total price of two orders is correctly calculated. The only thing we care about in this unit test is the price of both orders. But as you can see, to create an Order in a valid state, we need to set the Status and the user who owns the order as well. But to create the owning User we have to create it in a valid state as well. And in turn a User needs a valid Address instance. Such dependencies are quite common in for example your domain model (Hibernate/JPA entities).
We will also soon find ourselves copying and pasting the four lines of Order instantiation code in other tests that also require one or more valid Order instances. So not only it requires a lot of code to create an Order, we also copy and paste the same code all over the place.
Factories
Inspired by the “insert statement” technique we want a similar class that can help our tests create Order instances and provide default values for properties we don’t care about. Let’s call such a helper class a “factory”. This should reduce test code and allows us to only care about the price and not about any other fields of the order, or the fields of any of the order’s dependencies. This is what the factory looks like:
public class OrderFactory { private Status status; private User owner; private BigDecimal price; public OrderFactory() { // Initialize the factory with default values status = Status.SENT; owner = new UserFactory().newInstance(); price = new BigDecimal("9.95"); } public Order newInstance() { return new Order(status, owner, price); } public OrderFactory setPrice(BigDecimal price) { this.price = price; return this; } }
A factory should always have a no-arguments constructor which initializes defaults used for objects created by this factory. This way you can instantiate a factory without supplying any values at all. If a test does want to supply values, setter methods can be used.
Each factory is responsible for creating instances of a single class. This way you can easily instantiate this factory using only default values for the fields. Notice that the above factory delegates instantiating a new User to another factory. That UserFactory follows the exact same principle and should possibly delegate the creation of an Address to an AddressFactory. Each of these factories could be reused in other tests.
Having the OrderFactory in place helps us write the same test in a more efficient way:
@Test public void testCalculateTotalOrderPrice() { Order order1 = new OrderFactory().setPrice(new BigDecimal("15")).newInstance(); Order order2 = new OrderFactory().setPrice(new BigDecimal("35")).newInstance(); BigDecimal totalPrice = service.calculateTotalOrderPrice(Arrays.asList(order1, order2)); assertEquals(new BigDecimal("50"), totalPrice); }
Now we don’t have to care about anothing other than the price of an Order. All other required properties an Order has, we don’t have to set in the unit test. Also, dependencies of an Order are taken care of by the OrderFactory. Notice that we used a fluent interface for the factory so we can write the code for creating an Order in one statement. The fluent interface also makes the code more readable. Feel free to add any number of setter methods to any factory, as long as you only call them when your test cares about the value being set. Each of the three factory classes needed for this test (OrderFactory, UserFactory and AddressFactory) can also be reused in other tests. For example in tests that work with Users or with Addresses. As with InsertStatements never rely on defaults! (see Jelmer’s blog) Any value your test relies on should be made explicit, even if it is the same value as the default.
So as you can see, these “factories” use similar principles as the “insert statements”, but they apply to objects instead of database table rows. I have used factories for a while now and it makes writing unit tests take less time and more fun. Because once you have a lot of factories in place, new unit tests can easily (re)use them in other situations.