Abstract #
This article was originally about growing pains in test suites when using mocks. However, I realized there was a deeper fundamental issue: principles of design.
Mocks encourage reuse by inheritance, creating cumbersome fragile base test classes (e.g. AbstractFooBarTestCase
used across test case variations).
Mockists have a strong preference to test implementation details.
Additionally, mocks can blur architecture boundaries because no interface is required to test.
Both of these create coupled code.
Where as interfaces with fakes encourage composition. Writing to an interface creates an boundary, segmenting off unimportant implementation details. This enables decoupled code, modularity, and easier writing and maintenance of tests.
Ultimately, mocks themselves weren’t the issue. It was the arcane design effects that weren’t obviously apparent.
By following the two guiding principles from GOF:
- Program to an interface, not an implementation.
- Favor ‘object composition’ over ‘class inheritance’.
As well as following a Classical TDD approach of:
- A small public API to test outputs on, not the method details.
We can have a clean, decoupled test suite that’s resilent against change, particularly refactors.
Introduction #
In this article, we’ll explore mocks, why use them, common code smells with them, and a powerful alternative: writing to interfaces with assertive test fakes.
In our examples, we’ll look at a test case coupled because of concerns with
implementation details. We’ll observe how this causes brittle tests that
require more work to maintain during refactors.
Then, we’ll follow the design principle of Seperation of Concerns by creating an interface.
This simplifies mock setup and leads to a test question:
How do we test the implemented versions of an interface? Particularly those that require a database, web API, etc.
Finally, we’ll see an overall solution to these: replacing inheritance mock setups to composable assertive fakes.
I’m using PHP in the examples, apologies to those recoiling, please remain calm and keep all trunks and feet inside the vehicle. Modern PHP is just Java anyways.
Why Mocks? #
Mocks can be a great way to get started writing tests. They let you start testing objects with complex dependencies or that require third-party services (APIs, databases, etc.) without having to create or modify existing code. They often have fluent APIs too, which are neat to work with.
Consider the following CakeCommand class,
class CakeCommand
{
public function __construct(protected \PDO $database)
{
}
public function doThing(Cake $cake)
{
switch ($cake->type)
{
case 'icecream':
$cake->putInFreezer();
break;
// etc ... the 'business logic' details aren't important.
}
$stmt = $this->database->prepare('INSERT INTO cakes(type) VALUES(?);');
return $stmt->execute([$cake->type]);
}
}
To write a test for this, we’d have to have a MySQL instance running. This is easier with
containerization these days, but requires a lot of work to setup everything. The container,
the schema, fixture data, tear downs, etc. All of this requires time and creates a slow
complex test setup.
Mocks are a fast way to start testing our CakeCommand
class.
Example:
class CakeCommandTest extends TestCase
{
public function test_add_cake()
{
// From here,
$pdoMock = Mockery::mock(\PDO::class);
$statementMock = Mockery::mock(\PDOStatement::class);
$statementMock->shouldReceive('execute')
->with(['icecream'])
->andReturn(true);
$pdoMock->shouldReceive('prepare')->andReturn($statementMock);
// To here, a big chunk of code that's coupled to the test and
// implementation.
$decorator = new CakeCommand($pdoMock);
$icecreamCake = new Cake('icecream');
$decorator->doThing($icecreamCake);
$this->assertTrue($icecreamCake->is_in_freezer);
}
}
The code to setup the mock is a bit tedious, but easier than setting up a MySQL instance with schema and data fixtures. However, there’s a small detail (pun partially intended) in how we’ve setup the mock that will bite us later.
Devil in the Details #
For the purpose of this example, we’re taking a Mockist approach and verifying the method arguments of the database internals. Specifically this portion:
$statementMock->shouldReceive('execute')
->with(['icecream'])
->andReturn(true);
What arguments the PDOStatement
should have received. This isn’t important to the business of the CakeCommand class.
Saving to the database is overall an unimportant detail, the real core is what’s happening
to our domain Cake entity, what changes are being made to it and why?
Because of this coupling, look what happens when we want to make another call to
$decorator->doThing(...)
:
public function test_the_thing()
{
// From here,
$pdoMock = Mockery::mock(\PDO::class);
$statementMock = Mockery::mock(\PDOStatement::class);
$statementMock->shouldReceive('execute')
->with(['icecream'])
->andReturn(true);
$pdoMock->shouldReceive('prepare')->andReturn($statementMock);
// To here, a big chunk of code that's coupled to the test and implementation.
$command = new CakeCommand($pdoMock);
$icecreamCake = new Cake('icecream');
$command->doThing($icecreamCake);
$this->assertTrue($icecreamCake->is_in_freezer);
// Another call here:
$command->doThing(new Cake('redvelvet'));
}
Failure:
1) Tests\CakeCommandTest::test_the_thing
Mockery\Exception\NoMatchingExpectationException: No matching handler found for
Mockery_1_PDOStatement::execute([0 => 'redvelvet']).
This is what a fragile test is. We need to setup the mock again. A huge benefit of TDD is throw away tests. Poking at the system to figure out how things work. This removes that benefit. By not worrying about verifying method arguments, (as long as it saves correctly, i.e. returns true), we can keep extending this test without the baggage of having to update mocks.
Example, ->with(['icecream'])
removed.
public function test_the_thing()
{
// From here,
$pdoMock = Mockery::mock(\PDO::class);
$statementMock = Mockery::mock(\PDOStatement::class);
$statementMock->shouldReceive('execute')
->andReturn(true);
$pdoMock->shouldReceive('prepare')->andReturn($statementMock);
// To here, a big chunk of code that's coupled to the test and implementation.
$command = new CakeCommand($pdoMock);
$icecreamCake = new Cake('icecream');
$command->doThing($icecreamCake);
$this->assertTrue($icecreamCake->is_in_freezer);
// By removing the method argument details, we can keep poking
// at the core logic of the class, what happens to different
// types of cakes.
$command->doThing(new Cake('redvelvet'));
$command->doThing(new Cake('chocolate'));
}
Have discipline and confidence in not testing details. Decoupled tests are a joy to work in. Writing the test is more flexible, and you keep the benefit of throw aways. A useful addition to your toolbox in gaining valuable information on your codebase. The same mock exception would happen if we refactored the database methods in the class too.
Mockists at this point might be arguing, “How do we know for sure that the database is being called correctly and working? This test isn’t testing enough!”
My answer to that would be: an interface and humble test.
Separation of Concerns #
We’ve disciplined ourselves to not test the details of the database, but the mock setup is still awkward. We’re creating two mocks, one mock to return the other mock with specific details. We can create a reusable private method to cope, but what happens when another test requires slight variations? We have to copy paste the method to a slightly altered one, awkward and clunky.
Instead, we’ll follow SoC here and create an interface.
This encapsulates database saving details, shielding and simplifying the mock setup.
We could create a class here, but interfaces enables modularity not only in the codebase,
but also the test suite. We could create an InMemoryCakeRepository
or even a SqliteCakeRepository
too.
Whatever suits best.
Consider the new interface and updated class:
interface CakeRepository
{
public function addNew(Cake $cake): bool;
}
class DbCakeRepository implements CakeRepository
{
public function __construct(private \PDO $database)
{}
public function addNew(Cake $cake): bool
{
$stmt = $this->database->prepare('INSERT INTO cakes(type) VALUES(?);');
return $stmt->execute([$cake->type]);
}
}
class CakeCommandV2
{
public function __construct(protected CakeRepository $cakes)
{
}
public function doThing(Cake $cake)
{
switch ($cake->type)
{
case 'icecream':
$cake->putInFreezer();
break;
// etc ... the 'business logic' details aren't important.
}
return $this->cakes->addNew($cake);
}
}
The updated test:
class CakeCommandV2Test extends TestCase
{
public function test_the_thing()
{
$repoMock = Mockery::mock(CakeRepository::class);
$repoMock->shouldReceive('addNew')
->andReturn(true);
$command = new CakeCommandV2($repoMock);
$icecreamCake = new Cake('icecream');
$command->doThing($icecreamCake);
$this->assertTrue($icecreamCake->is_in_freezer);
$command->doThing(new Cake('redvelvet'));
}
}
With this refactor, we’ve encapsulated the database to an abstract CakeRepository
,
we’ve written to an interface.
This not only cleaned up our CakeCommand
class, but our test as well.
We no longer have to have a two mock setup, with awkward PDOStatement
methods to fake.
The Humble Test #
Now to fully answer the mockist’s question.
How to verify the database is being called correctly? Write a test that’s only ran manually during development or during refactors said class, and once before a merge if you want. I coined this the Humble Test, inspired by the Humble Object. The relation is that Humble Objects are manually verified, e.g. visually inspecting a GUI. Here, Humble Tests are typically ran manually, or very little because of their nature. Another example would be a Humble Test to verify a custom web API class works correctly.
I’ve seen something like this called an integration test, but by modern definition that’s something completely different.
Many unit test suites have configurations to mark a test with a specific named subset, allowing you to run a “humble” test suite say only during pre-merge. PHPUnit has the @group annotation.
Example:
/**
* @group humble
*/
class DbCakeRepositoryTest extends TestCase
{
protected \PDO $db;
protected function setUp(): void
{
parent::setUp();
$this->db = new \PDO('mysql:host=db;dbname=foo', 'root', '');
$this->db->exec('DROP TABLE IF EXISTS `cakes`;');
$createTableSql = <<<SQL
CREATE TABLE `cakes` (
id int NOT NULL AUTO_INCREMENT,
`type` VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
);
SQL;
$this->db->exec($createTableSql);
}
public function test_add_new()
{
/*
* If you don't want to use a @group type annotation,
* uncomment while writing the DbCakeRepository class.
*/
// $this->markTestSkipped('Only run manually');
$repo = new DbCakeRepository($this->db);
$cake = new Cake('ice-cream');
// Act
$repo->addNew($cake);
// Assert
$stmt = $this->db->prepare(<<<SQL
SELECT COUNT(*) as count FROM cakes WHERE `type` = 'ice-cream'
SQL);
$stmt->execute();
$result = $stmt->fetch(\PDO::FETCH_OBJ);
$this->assertEquals(1, $result->count);
// etc..
}
}
I should note that there’s nothing wrong with touching the database or file system in a test suite, as long as it’s fast. Third party web APIs, different story.
Assertive Fakes #
Okay, so far we’ve seen the benefits of removing detail testing, simplified the mock setup with encapsulation, and verified our DbCakeRepository
is working correctly.
But what if you still want to verify that CakeRepository
was called correctly in your tests?
This is possible without mocks, but requires a little leap of faith.
We verify calls indirectly, in a style I like to call an Assertive Fake.
From Wikipedia:
Fake — a relatively full-function implementation that is better suited to testing than the production version; e.g. an in-memory database instead of a database server
Assertive Fakes are just fakes with assertion methods on them. They solve the hassle of setting up detailed mocks, while retaining the testability of the interface, allowing us to make sure the interface was called with the expected details.
I’ve also configured versions of them to force an error on a method call, if you really want to go that far. This is the great thing about them, is it’s your own code. You’re not married to a mocking framework, you can create any tool or gizmo to get the level of testing you want.
Another benefit, since it’s just a class that satisfies the interface behavior,
if you don’t need any extra assertions, that’s fine. You don’t
have to call them. This new CakeRepositoryFake()
expression, is a lot simpler than setting
up a complicated mock, specifiying what detailed behavior you want. Not having to manage
mock setups through private methods in an AbstractFooTestCase
is a blessing. Adhering to the
principle of “Prefer composition, over inheritance.”
Example:
class CakeRepositoryFake implements CakeRepository
{
public $cakes = [];
public function addNew(Cake $cake): bool
{
$this->cakes[] = $cake;
return true;
}
public function assertContains($cake)
{
TestCase::assertTrue($this->contains($cake), "cake `{$cake->type}` not found");
}
public function contains($cake)
{
foreach ($this->cakes as $ck) {
if ($cake == $ck) {
return true;
}
}
return false;
}
}
// @see: http://docs.mockery.io/en/stable/getting_started/quick_reference.html
class CakeCommandV3Test extends TestCase
{
public function test_the_thing()
{
$repo = new CakeRepositoryFake();
$command = new CakeCommandV2($repo);
$icecreamCake = new Cake('icecream');
// Act
$command->doThing($icecreamCake);
// Assert
$this->assertTrue($icecreamCake->is_in_freezer);
$repo->assertContains($icecreamCake);
// We're not coupled to the mock, we can keep calling the method.
// But now, we can now verify the interface received what we expect.
$redVelvet = new Cake('redvelvet');
$command->doThing($redVelvet);
$repo->assertContains($redVelvet);
$birthday = new Cake('birthday');
$command->doThing($birthday);
$repo->assertContains($birthday);
}
}
This is a Classical approach to TDD. The implementation details don’t matter, and we can verify it was called correctly in an indirect manner. This adheres to the principle of testing behavior, not implementation.
Conclusion #
We’ve seen a lot so far, the utility of mocks, the brittleness when testing detail implementation over behavior, and solutions to those. Discipline in testing a small public API, the outputs, the class behavior. Mocks make it too easy to reach into the internals of classes, binding themselves to private methods and inner workings of a class.
This smothers refactors, disregarding a core benefit of testing & TDD: The ability to tear down, rebuild, and experiment with how the class gets to its goal. As long as the tests are concerned with the final output, the behavior of the unit, you can refactor all day long and your tests won’t fracture and break.
You should definitely not be testing private or protected methods. The majority of refactors should be on how a class does its work. The users of a class don’t care how the object accomplishes its job, it just knows what inputs to give and what outputs it wants. Your tests should be the same.
Then, we went a step further and created an architecture boundary with an interface. This encapsulated repository details and paved a path towards humble tests and assertive fakes. This led us to the common Mockist’s question, of “How do we know that our interface is being called correctly?” and answered it two steps.
First, we can verify our database code works with a separate test. This cleaned up our first CakeCommandTest, simplifying its setup and testing concerns.
Then, we saw that Assertive Fakes are also capable of verifying details
of what arguments were passed to it (albiet, indirectly).
This gave us the power of detailed assertions from mocks, with clean re-useable, decoupled test doubles.
The best part is it’s at our discretion. Setting up mocks for verification can be tedious and repetitive,
and falls into the AbstractCakeTestCase
fragile base class problem.
Using fakes adheres to both principles of the GOF book:
- Program to an interface, not an implementation.
- Favor ‘object composition’ over ‘class inheritance’.
As well as the Classical TDD princple of:
- Test behavior, not implementation.
A test suite is just another code base, treat it as such. By applying fundamental design princples, discovered almost thirty years ago, we can have our cake and eat it too. A decoupled resilient test suite thats a delight to work in.
I encourage you to learn more about testing and TDD history. I was exposed to a lot of dogma, and frankly prefer a Classical TDD approach than the modern Mockist style. I’ve linked a great talk below and highly recommend the original TDD book from Kent Beck. Where vultures are circling, there be a corpse. Go directly to the source, hear it from the horse’s mouth.
I hope I’ve shined a light on things and that you’ve gleaned some new tools to use in your programming journey.
Thank you for reading.
Further Resources / Reading #
- TDD, Where Did It All Go Wrong (Ian Cooper)
- Is TDD Dead?
- Is TDD Dead [Part II]
- Mocks Aren’t Stubs (Martin Fowler):
- London vs. Chicago (Clean Coders) (Yes I paid for this, but it was rewarding to see the Mockist style approach I was exposed to at a Java shop. The amount of busy work it creates and a better alternative)
(NOT Amazon affiliate links)
- Test Driven Development (Kent Beck): https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530
- Clean Architecture (Robert Martin): https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164