Programming Seeking Understanding
The blog of Mark Cheeseman

Design Patterns in Testing, the Builder

Following on from Ayende’s recent post on the builder pattern in his design patterns series, I thought it would be interesting to discuss one of most useful purposes I have recently experienced the builder pattern used for.

Integration tests typically require putting the system into a certain state to perform the test. Baring certain minimal data that may be assumed to be pre-existing in a database, the setup can still amount to a considerable amount of work. Having a collection of builders to produce sample aggregates within a system can assist greatly in this task.

For any given aggregate, it should aways be possible to do something like this:

var quote = new QuoteBuilder().Build();
// Perform test for which a quote is required.

Doing so would result in a valid quote being created. The state of that quote is completely irrelevant. The sole guarantee of the builder is that by default, it will build a completely valid aggregate without having to fuss over any details at all.

Builders also allow for the setting of state when that is important to the test. For instance, if the customer with specific properties was required, the following might be done:

// Arrange
var customer = new CustomerBuilder().SetOverCreditLimit().Build();
var quote = new QuoteBuilder().SetCustomer(customer).Build();
// Act
// perform test on the quote where the customer is of significance to the test.

Although this can result in builder setter functions which do little more than setting a value within the aggregate, the benefit is that only properties that matter to the test appear in the test. Consequently, the nature of what matters to the test is evident, simply by reading the test.

Sometimes, more complicated relationships exist between aggregates. For example, to setup a quote which could be used for a particular shipment it might be necessary to do something like:

// Arrange
var shipment = new ShipmentBuilder().Build();
var quoteMatchingShipment = new QuoteBuilder()
    .SetCustomer(shipment.Customer)
    .SetValidFrom(shipment.Eta.AddDays(-1)
    .SetValidTo(shipment.Eta.AddDays(1)
    .AddTradelane(shipment.Origin, shipment.Destination);

Consequently, it becomes useful to establish relationships between aggregates more simply via methods that are scenario based. For instance:

// Arrange
var shipment = new ShipmentBuilder().Build();
var quoteMatchingShipment = new QuoteBuilder().Match(Shipment).Build();
// Act
// perform test where it is required for the quote to apply to the shipment.

The stateful nature of the builders can make for some interesting patterns. For example, if one wanted a quote which matched a shipment in all ways except for the customer, the following is possible:

// Arrange
var shipment = new ShipmentBuilder().Build();
var quoteNearlyMatchingShipment = new QuoteBuilder()
    .Match(shipment)
    .SetCustomer(new CustomerBuilder().Build())
    .Build();
// perform test that makes sure that a quote matching in all regards 
// except customer is not applied to the shipment.

This relies on the fact that setting the customer after matching the shipment clobbers the customer match that was set-up as part of .Match(shipment).

Some of the benefits in having aggregate builders for integration tests include:

However, the biggest benefits arise from the way it compels developers to think about their testing. Assuming the aggregates are correctly identified with respect to the business, the test setup using builders is easily envisaged as closely corresponding to what actually happens in the business. This stands in contrast to setup which appears to have little correspondance to the business, or at least, where that correspondance is hidden behind a wall of property setting code that obscures what is the essence and what is incidental to the test.