December 1, 2007 12:00 AM

Tricks with Test Data Builders: Refactoring Away Duplicated Logic Creates a Domain Specific Embedded Language for Testing

Builder and Customer

Test Data Builders remove a lot of duplication from test code, but there can often still be duplicated logic at the point at which the built objects are used. Many different tests will have very similar code that creates an object using a builder and then passes it to the code under test. We can address this duplication by factoring out test scaffolding that works with builders, not system objects. Doing so produces a higher level testing API that more clearly communicates the intent of the test and hides away unimportant details of how the system is being tested.

For example, consider a system to process orders. Orders are sent into our system and processed asynchronously. To perform an end-to-end system test, the test must must create an order, send the order to our system and track the processing of the order by waiting for correlated events to appear on the system's monitoring topic and driving the client through its user interface. That would look something like the following (where the requestSender and progressMonitor do lots of behind the scenes magic with JMS connections, sessions, message producers and consumers, message properties and correlation IDs).

@Test public void reportsTotalSalesOfOrderedProducts() {
    Order order1 = anOrder()
        .withLine("Deerstalker Hat", 1)
        .withLine("Tweed Cape", 1)
        .withCustomersReference(1234)
        .build();

    requestSender.send(order1);
    progressMonitor.waitForConfirmation(order1);
    progressMonitor.waitForCompletion(order1);

    Order order2 = anOrder()
        .withLine("Deerstalker Hat", 1)
        .withCustomersReference(5678)
        .build();

    requestSender.send(order2);
    progressMonitor.waitForConfirmation(order2);
    progressMonitor.waitForCompletion(order2);

    TotalSalesReport report = gui.openSalesReport();
    report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
    report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}

It is tempting pull this duplication into a "helper" method that builds and uses an object. For example:

@Test public void reportsTotalSalesOfOrderedProducts() {
    submitOrderFor("Deerstalker Hat", "Tweed Cape");
    submitOrderFor("Deerstalker Hat");

    TotalSalesReport report = gui.openSalesReport();
    report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
    report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}

void submitOrderFor(String ... products) {
    OrderBuilder orderBuilder = anOrder()
        .withCustomersReference(customersReference++);

    for (String product : products) {
        orderBuilder = orderBuilder.withLine(product, 1);
    }

    Order order = orderBuilder.build();
    
    requestSender.send(order);
    progressMonitor.waitForConfirmation(order);
    progressMonitor.waitForCompletion(order);
}

private int customersReference = 1;

However, this refactoring leaves us with the same difficulties that we encountered with the Object Mother when we have to vary data in different tests. We will need to submit orders with different properties and submit different kinds of events — orders, order amendments, order cancellations, etc. The helper method has the very same problems we found with the Object Mother, and that we avoided by using builders to create our test data.

void submitOrderFor(String ... products) { ... }
void submitOrderFor(String product, int count) { ... }
void submitOrderFor(String product, int count, String otherProduct, int otherCount) { ... }
void submitOrderFor(String product, double discount) { ... }
void submitOrderFor(String product, String giftVoucherCode) { ... }
... etc ...

Instead, we can pass an order builder to the method that sends an order into the system, just as we do when combining builders. That method can add properties through the builder before building the order sending it into the system.

@Test public void reportsTotalSalesOfOrderedProducts() {
    sendAndProcess(anOrder()
        .withLine("Deerstalker Hat", 1)
        .withLine("Tweed Cape", 1));
    sendAndProcess(anOrder()
        .withLine("Deerstalker Hat", 1));
    
    TotalSalesReport report = gui.openSalesReport();
    report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
    report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}

void sendAndProcess(OrderBuilder orderDetails) {
    Order order = orderDetails
        .withDefaultCustomersReference(customersReference++)
        .build();
    
    requestSender.send(order);
    progressMonitor.waitForConfirmation(order);
    progressMonitor.waitForCompletion(order);
}

private int customersReference = 1;

Finally, a bit of judicious renaming can change the language of the test so that it communicates more about what behaviour is being tested than how the system implements that behaviour.

@Test public void reportsTotalSalesOfOrderedProducts() {
    havingReceived(anOrder()
        .withLine("Deerstalker Hat", 1)
        .withLine("Tweed Cape", 1));
    havingReceived(anOrder()
        .withLine("Deerstalker Hat", 1));
    
    TotalSalesReport report = gui.openSalesReport();
    report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
    report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}

@Test public void takesAmendmentsIntoAccountWhenCalculatingTotalSales() {
    Customer theCustomer = aCustomer().build();

    havingReceived(anOrder().from(theCustomer)
        .withCustomerReference(10)
        .withLine("Deerstalker Hat", 1)
        .withLine("Tweed Cape", 1));
        
    havingReceived(anOrderAmendment().from(theCustomer)
        .withCustomerReference(10)
        .withLine("Deerstalker Hat", 2));

    TotalSalesReport report = gui.openSalesReport();
    report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
    report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}

Test Data Builders are a foundation upon which we can define higher-level testing APIs that better communicates the intent of our tests in a language that is closer to that used by non-technical project stakeholders and so greatly help communication within the project.

Update: Thanks to David Peterson and Michael Hunger for helpful feedback. I've fixed typos in the test code and improved the test names. Hopefully the code is easier to follow now.

Posted on December 21, 2007 [ Permalink | Comments (0) ]

Tricks with Test Data Builders: Emphase the Domain Model with Factory Methods

Construction of the 'gherkin' building

Tests that use Test Data Builders can be made less noisy by combining builders. This still leaves some noise in the test: the test code overly emphasises how the tests are building objects at the expense of what they are building. A future reader of the test will be far more interested in what objects are being used than in the way that those objects are constructed.

We can de-emphasise the builders further by instantiating them in clearly named factory methods:

Order order = 
    anOrder().fromCustomer(
          aCustomer().withAddress(
              anAddress().withNoPostcode())).build();

When we do this, the naming convention we've used for builder methods up to now gets in the way instead of making things clearer. The builder code looks better if we rename the methods to reflect the relationship between objects only, and not include the type of object at the far end of the relationship:

Order order = 
    anOrder().from(aCustomer().with(anAddress().withNoPostcode())).build();

This relies on Java's method overloading and so only works for properties that have unique, user-defined types. Longer method names are necessary for primitive types, or if the built object has different relationships with the same type of object. For example, most of the fields of an Address are Strings, and so the builder methods must be explicitly named after the field. However, the post code is strongly typed and so can be passed to an overloaded method:

Address aLongerAddress = anAddress()
    .withStreet("222b Baker Street")
    .withCity("London")
    .with(postCode("NW1", "3RX"))
    .build();
Posted on December 16, 2007 [ Permalink | Comments (0) ]

Tricks with Test Data Builders: Combining Builders

Three Builders

If an object built with a Test Data Builder contains other objects built with other Test Data Builders, you can pass one builder to another to save keystrokes and reduce noise, making tests easier to read. For example, instead of:

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

You can write:

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

The result is significantly easier to read.

Posted on December 13, 2007 [ Permalink | Comments (0) ]

Tricks with Test Data Builders: Defining Common State

A builder with a plan

Using separate Test Data Builders to construct objects with common state leads to duplication and can make the test code harder to read and maintain. For example:

Invoice invoiceWith10PercentDiscount = new InvoiceBuilder()
    .withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
    .withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12))
    .withDiscount(0.10)
    .build();

Invoice invoiceWith25PercentDiscount = new InvoiceBuilder()
    .withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
    .withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12))
    .withDiscount(0.25)
    .build();

Instead, you can initialise a single builder with the common state and then repeatedly call its build method after defining values that apply only to the built objects:

InvoiceBuilder products = new InvoiceBuilder()
    .withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
    .withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));

Invoice invoiceWith10PercentDiscount = products
    .withDiscount(0.10)
    .build();

Invoice invoiceWith25PercentDiscount = products
    .withDiscount(0.25)
    .build();

This can make tests much easier to read because there is less code and you can give the builder a descriptive name.

However, you have to be careful if the built objects need different fields to be initialised. Because the withXXX methods change the state of the shared builder, objects built later will be created with the same state as those created earlier unless it is explicitly overridden. For example, in the following code, the second invoice has both a discount and a gift voucher, which is not what the code appears to communicate at first glance.

InvoiceBuilder products = new InvoiceBuilder()
    .withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
    .withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));

Invoice invoiceWithDiscount = products
    .withDiscount(0.10)
    .build();

Invoice invoiceWithGiftVoucher = products
    .withGiftVoucher("12345")
    .build();

A solution is to add a method or copy constructor to the builder that copies state from another builder:

InvoiceBuilder products = new InvoiceBuilder()
    .withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
    .withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));

Invoice invoiceWithDiscount = new InvoiceBuilder(products)
    .withDiscount(0.10)
    .build();

Invoice invoiceWithGiftVoucher = new InvoiceBuilder(products)
    .withGiftVoucher("12345")
    .build();

Alternatively, you could add a factory method to the builder that returns a new builder with a copy of the builder's state:

InvoiceBuilder products = new InvoiceBuilder()
    .withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
    .withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));

Invoice invoiceWithDiscount = products.but().withDiscount(0.10)
    .build();

Invoice invoiceWithGiftVoucher = products.but().withGiftVoucher("12345")
    .build();

The safest option is to make every with method create an entirely new copy of the builder instead of returning this.

Posted on December 12, 2007 [ Permalink | Comments (0) ]

XTC has moved

XTC logo

For the last eight years or so, London's famous Extreme Tuesday Club (XTC) has been meeting every Tueday to share ideas about XP, "agile" software development, test-driven development and otherwise waffle philosophise about software development over fine ale and microwaved, puff-pastry pies (being in the UK, the beer is much better than the food).

The last few years, we have met in the Old Bank of England pub on the Strand. From now on we'll be in the Counting House pub on Cornhill, near Bank tube station.

Lots of great ideas and collaborations have spun out of the XTC, among them the XP Day family of grass-roots conferences, the mock objects technique and the jMock library for test-driven development of object-oriented code, the First International Conference on Postmodern Programming, the Jester mutation testing tool, the Extreme Lego workshop and other training games, and lots more.

An interesting aspect of XTC is that it is completely anarchic. There is no central organising committee. There is no formal membership. What happens is entirely up to whoever turns up and gets involved. This year we will be spending some of the income from XP Day on regularly hosting more formal presentations and rerunning the most popular sessions from XP Day to make them available to a wider audience. But for the rest of the time, the informal get-together style will prevail.

So, if you're interested in the future direction of software development techniques and practices, do please come along the Counting House and join in. The beer is the same but the pies are better, I've been told.

Posted on December 2, 2007 [ Permalink | Comments (0) ]