As you start to get better at creating well formed objects, that are immutable or protect their own invariants, constructing the necessary fixtures for your tests can become quite repetitive and our test arrangement can start to get unwieldy. The first bit of refactoring we might do to improve things could be to extract creation methods, but this doesn't scale so well beyond a single test file.
One of the many things I picked up from the Growing Object Oriented Software book was the use of Object Mothers.
Object Mothers are a kind of factory, producing canned test fixtures for use directly in your tests, or to help constructing other test fixtures. They help to remove duplication and make it easy to create complex fixtures.
class ExampleEmployees
{
public static function activeEmployee()
{
return Employee(
EmployeeId::generate(),
new Name("Jack Burton"),
ExampleEmploymentHistory::emptyHistory()
);
}
}
Once created, they can be used in a number of tests and really help to keep your tests concise.
/** @test */
public function it_allows_active_employees_to_login()
{
$employee = ExampleEmployees::activeEmployee();
$spec = new IsAllowedToLogin();
$this->assertTrue($spec->isSatisfiedBy($employee));
}
When they're working well, Object Mothers are very simple and very effective. I use them daily and you should definitely try them out, but be wary, they can quickly become quite cumbersome.
class ExampleEmployees
{
public static function newStarter()
{
return new Employee(
EmployeeId::generate(),
new Name("Jack Burton"),
ExampleEmploymentHistory::emptyHistory()
);
}
public static function seasonedDeveloper()
{
return new Employee(
EmployeeId::generate(),
new Name("Gracie Law"),
ExampleEmploymentHistory::fromJuniorToSeniorDeveloper()
);
}
}
This example shows quite a few traits of Object Mothers. The first thing to know about Object Mothers is that tests can and therefore will depend on some of the canned values contained within the objects.
Take the following two tests, at the
first glance they look really neat and tidy, and will have been pretty easy to
write. Looking more closely, without seeing the production code, we'd have to
assume that the seasonedDeveloper
's employment
history, contains the necessary experience that the
IsSuitableForFireSafetyOffice
specification requires. This could be as simple as years in
service, or it could be as complex as having experience in particular positions.
Unlike our first example that explictly showed we were creating an active
employee, and testing that an active
employee could login, our specifics are a
little more vague here.
/** @test */
public function it_rejects_less_experienced_employees()
{
$spec = new IsSuitableForFireSafetyOfficer();
$this->assertTrue($spec->isSatisfiedBy(ExampleEmployees::newStarter()));
}
/** @test */
public function it_accepts_experienced_employees()
{
$spec = new IsSuitableForFireSafetyOfficer();
$this->assertTrue($spec->isSatisfiedBy(ExampleEmployees::seasonedDeveloper()));
}
I'm not keen on the coupling this brings. Even though our object mother methods
contain the words new
and seasoned
, so do communicate some notion of
experience, they aren't precise enough for me. Ian Cooper
describes this
coupling as a form of Shared
Fixture, in that a developer
changing the value for one of the examples for a particular test, could break
another test somewhere else that depended on the same value, meaning we have
fragile tests.
The fact that the Object Mothers become a global place for
defining the canned objects does alleviate some of the coupling concerns, as all
programmers on your team will know exactly where to look to inspect how the
fixtures are created. In the Martin Fowler
article, he actually mentions
that some teams totally embrace this to the point where they give the example
objects personas, such that the methods in the examples above might be known as
Jack and Gracie, and their attributes are well known. So, it is well known
within the team that Gracie (our seasonedDeveloper
) has enough experience to
be a fire safety office, and this attribute should not be changed, much like the
active attribute of the activeEmployee
example. As long as the team understand
and can recall the attributes of each persona or know exactly where to look when
they need to, it might not be too much of a problem, but isn't something I feel
too comfortable with.
Another one of the problems with object mothers, is that the canned objects they create are just that, canned objects. As a consumer, we're restricted in the types of objects we can get from them, without adding more specific methods. Sometimes this can be a good thing, you get a fairly generic object from the Object Mother and then customise it in the test method. This helps to show a lot of intent within the test.
/** @test */
public function it_accepts_anyone_with_the_fire_safety_training()
{
$employee = ExampleEmployees::newStarter()
->award(new FireSafetyTrainingCompetency());
$spec = new IsSuitableForFireSafetyOfficer();
$this->assertTrue($spec->isSatisfiedBy($employee));
}
The problem really arises when the change we need to make to a generic example object to be more specific, can't be done after instantiation, either because the whole object is immutable or perhaps just the part we're interested in is immutable. It's tempting to add another method to the Object Mother, to keep the complexity of creating the fixture out of the test and available for reuse in other tests.
/** @test */
public function it_accepts_anyone_who_has_previously_been_a_fire_safey_officer()
{
$employee = ExampleEmployees::previouslyAFireSafetyOfficer();
$spec = new IsSuitableForFireSafetyOfficer();
$this->assertTrue($spec->isSatisfiedBy($employee));
}
As you can imagine, this can quickly get out of hand if you need a lot of
different example employees. Not only will there be a lot of examples for people
to be familiar with, but the code required to create many such examples can quickly
become complicated. Looking back at the examples, we already have duplication,
in that the activeEmployee
and newStarter
examples are exactly the same. We
could remove the duplication by having one aliased to the other, but that brings
it's own complexities. One possible option to alleviate this problem is to add
options and arguments to the methods, but this is really just creating a
different kind of coupling and would probably only be a quick fix for a short
while, but quickly crash and burn.
This post has probably come over more negatively towards Object Mothers than I had intended, but it turns out that the downsides to Object Mothers take more explanation than the good bits. However, having read this, I do suggest you now take a look at Test Data Builders, a better alternative to Object Mothers.