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.