I recently covered Object Mothers, a pattern that provides factory methods for creating example objects for use in tests. That post showed how Object Mothers are effective to begin with, but can quickly become unmanageable. A better alternative to Object Mothers is to use the Builder pattern.

Nat Pryce has a series of great posts on this subject, which I'm going to pull together here in a briefer format and hopefully still do justice. I'm going to use his example of invoices with line items and each heading on this post links to Nat's more detailed post. It is important to know that each subsequent section either builds on the previous section or simply suggests tricks to get more out of the pattern. It is not necessary to implement each section to benefit from using builders, but they can make your builders more enjoyable and legible. Jay Fields also uses them extensively in his book Working Effectively With Unit Tests, which I highly reccomend.

Builders1

The biggest problem you might come across with object mothers, is when you start to require lots of different variations of the same thing, where these variations need to be known at construction time. Your choice is to compromise the object model, making things mutable when they shouldn't be so you can customise more general example objects in the test methods, or create really long and silly method names:

$invoice = ExampleInvoice::withRobeAndWizardHat();
$invoice = ExampleInvoice::withPadAndPen();
$invoice = ExampleInvoice::withRobeWizardHatPadAndPen();

The answer is to use the Builder pattern. You're going to use it just like you would do with production code, but have all options pre-filled with example data.

$anyOldInvoice = (new ExampleInvoiceBuilder)->build();
$bigSpendersInvoice = (new ExampleInvoiceBuilder)
    ->withLine("Robe", Money::GBP(1000))
    ->withLine("Wizard Hat", Money::GBP(500))
    ->withLine("Pad", Money::GBP(300))
    ->withLine("Pen", Money::GBP(200))
    ->build();

Immutable Builders2

Even with these awesome builders, we can quickly find duplicate code in our test arrangement. Take the following example:

$invoiceWith10PercentDiscount = (new ExampleInvoiceBuilder)
    ->withLine("Robe", Money::GBP(1000))
    ->withLine("Wizard Hat", Money::GBP(500))
    ->withDiscount(0.10)
    ->build();

$invoiceWith25PercentDiscount = (new ExampleInvoiceBuilder)
    ->withLine("Robe", Money::GBP(1000))
    ->withLine("Wizard Hat", Money::GBP(500))
    ->withDiscount(0.25)
    ->build();

We can avoid this duplication by making the builders immutable. Whenever one of the customisation methods is called, we return a new instance of the builder, allowing us to:

$invoiceWithRobeAndWizardHat = (new ExampleInvoiceBuilder)
    ->withLine("Robe", Money::GBP(1000))
    ->withLine("Wizard Hat", Money::GBP(500));

$invoiceWith10PercentDiscount = $invoiceWithRobeAndWizardHat
    ->withDiscount(0.10)
    ->build();

$invoiceWith25PercentDiscount = $invoiceWithRobeAndWizardHat
    ->withDiscount(0.25)
    ->build();

Immutable Builders with Immutable Builders3

Once you start building everything, your test arrangement might look like:

$invoiceWithRobeAndWizardHat = (new ExampleInvoiceBuilder)
    ->withLine((new ExampleInvoiceLineBuilder())
        ->withProduct((new ExampleProductBuilder())
            ->withName("Robe")
            ->withPrice(Money::GBP(1000))
            ->build())
        ->build())
    ->build();

Things look a little bit cleaner if you allow your builders to take other builders, and build them when they need to.

$invoiceWithRobeAndWizardHat = (new ExampleInvoiceBuilder)
    ->withLine((new ExampleInvoiceLineBuilder())
        ->withProduct((new ExampleProductBuilder())
            ->withName("Robe")
            ->withPrice(Money::GBP(1000))))
    ->build();

Factory methods for Immutable Builders4

Let's clean things up even further. Firstly, where we're passing objects to the builders, we can probably do away with the individual methods and do some pseudo method overloading with one with method:

$invoiceWithRobeAndWizardHat = (new ExampleInvoiceBuilder)
    ->with((new ExampleInvoiceLineBuilder())
        ->with((new ExampleProductBuilder())
            ->withName("Robe")
            ->with(Money::GBP(1000))))
    ->build();

I've left the withName method as it was. The builder can't effectively know what a primitive value like Robe would actually be without the specific method name, we might not even know for sure ourselves. As an aside, I don't think with(Money::GBP(1000)) is quite clear enough for me, listening to the tests I'd probably ask if I needed a more descriptive type.

Now we've done that, we can go and build some factory methods to create the builders themselves. Nat imports static methods, we don't have that in PHP, but we do have functions and as of 5.6, we can import them. Let's assume we write these and import them:

function anInvoice() { return new ExampleInvoiceBuilder() }
function aLine() { return new ExampleInvoiceLineBuilder() }
function aProduct() { return new ExampleProductBuilder() }

Our test arrangement can now become:

$invoiceWithRobeAndWizardHat = anInvoice()
    ->with(aLine()
        ->with(aProduct()->withName("Robe") ->with(Money::GBP(1000))))
    ->build();

We've hidden the fact that we're using builders using nice factory methods, and removed a fair bit of clutter. Whether it's functions or static methods like an::invoice, it doesn't really matter, so long as we've cleaned things up.

We can do better though, those withXXX methods don't quite roll off the tongue in this context. Let's make them more expressive:

$invoiceWithRobeAndWizardHat = anInvoice()
    ->with(aLine()
        ->for(aProduct()->called("Robe") ->pricedAt(Money::GBP(1000))))
    ->build();

An invoice with a line for a product called Robe priced at Money::GBP(1000). I can live with that last bit ;)

Flexible DSL with Immutable Builders5

Not only can our builders use builders, but we can start to pass builders around to our test helper methods as well.

Take the use of a custom verification method from my introduction to reducing duplication in test code:

/** @test */
public function test_premium_user_can_login()
{
    $user = User::register("dave@example.com", "password123");
    $user->upgrade();
    $this->repo->add($user);

    $this->verifyUserCanLogin("dave@example.com", "password123");
}

The test itself doesn't really care for the email address and password, but they are required to operate the test (I am making an assumption that password gets hashed and can't be retrieved from the user object).

This creates duplication, as every test that wants to verify some type of user can log in needs to create an email address and password. The verification method verifyUserCanLogin itself can also become a problem spot, as it suffers from the same problem as the Object Mothers, where variations could lead to complicated method signatures and/or extra methods for slightly different use cases.

If we were to change the verifyUserCanLogin method to accept an ExampleUserBuilder, we can pass in a builder where the premium flag has been set, leaving the custom verification method to take care of the irrelevant details.

/** @test */
public function test_premium_user_can_login()
{
    $this->verifyUserCanLogin(aUser()->whoIsPremium());
}

private function verifyUserCanLogin(ExampleUserBuilder $exampleUser)
{
    $user = $exampleUser
        ->thatAuthenticatesWith("user@example.com", "password123")
        ->build();

    $this->repo->add($user);

    $response = $this->client->post('/login_check', [
        'email' => "user@example.com", 
        'password' => "password123",
    ]);

    $this->assertEquals(200, $response->getStatusCode());
    $this->assertContains('Login Successful', $response->getContent());
}

Going forward, any other requirements for the test to be able to verify the user can login can expand on the builder. This example is a little weak, it doesn't create a particular nice DSL like Nat's post, but you should be able to see the possibilities.

This is my fourth post in four weeks and I don't want that streak to come to an end! If you feel like you have difficulties writing better tests, drop me an email, ping me on twitter or catch me freenode in #thatpodcast, I'm always looking for things to write about.