The Difference Between Mocks and Stubs

While I've been writing tests for my PHP code for years, I'm still learning. One of my recent learnings was understanding the difference between mocks and stubs - and when to use each one. In this article, I'm going to share that difference and why it's important.

Want to learn more about Docker?

Are you tired of hearing how "simple" it is to deploy apps with Docker Compose, because your experience is more one of frustration? Have you read countless blog posts and forum threads that promised to teach you how to deploy apps with Docker Compose, only for one or more essential steps to be missing, outdated, or broken?

Check it out

But first, how did I come to learn the difference? Well, honestly, it only happened quite recently; likely because I started using PHPUnit 12.5. I was working on a small IVR (Interactive Voice Response) application in PHP, when I saw the following error:

9) AppTest\ApplicationTest::testAppHandlesRequestsCorrectly#8 with data ('provide-policy-number')
No expectations were configured for the mock object for Psr\Http\Message\ResponseInterface. Consider refactoring your test code to use a test stub instead. The #[AllowMockObjectsWithoutExpectations] attribute can be used to opt out of this check.

Below is a, slightly shorter version of the code which the notice above is referring to. Specifically, it's referring to $this->createMock(ResponseInterface::class), the second argument passed to the getMenu() function at the end of the function.

#[TestWith(['choose-department'])]
#[TestWith(['choose-insurance-category'])]
#[TestWith(['choose-insurance-type'])]
#[TestWith(['provide-personal-details'])]
#[TestWith(['provide-policy-number'])]
public function testAppHandlesRequestsCorrectly(string $step): void
{
    $container = $this->createStub(Container::class);

    AppFactory::setContainer($container);
    $slimApp = AppFactory::createFromContainer($container);

    $app = new Application(
        $slimApp,
        new TwiMLService(new VoiceResponse()),
        $this->createStub(SessionInterface::class),
    );

    $request = $this->createMock(ServerRequestInterface::class);
    $request
        ->expects($this->once())
        ->method('getParsedBody')
        ->willReturn(
            [
                'Digits' => "1",
            ],
        );

    $menu = $app->getMenu(
        $request,
        $this->createMock(ResponseInterface::class),
        ['step' => $step],
    );
}

At first, I didn't really know what the fuss was about. Why is this an issue. If I pass a mock or a stub, what's the big deal? Aren't they, basically, the same thing?

Laugh or scoff you well may if your testing knowledge is a lot more intricate than mine; which will likely include me in the coming years. However, up until now, I've not looked too deeply at the differences between mocks and stubs. And, to be honest, feel that I've written some solid test suites without having to really appreciate the difference either.

But, as I felt that I'd not progressed in my testing knowledge for some time, I saw this as an excellent opportunity to learn and grow. So to do that, I googled the key point from the error message:

Consider refactoring your test code to use a test stub instead.

The first meaningful article that I found in the search results was "Refactor tests to use stubs instead of mocks where mocks do not configure expectations - in component tests" on Drupal.org. This, in turn, lead me to an excellent article Testing with(out) dependencies by PHPUnit's creator Sebastian Bergmann. In that article, Sebastian provided a succinct distinction between Stubs and Mocks.

Without rehashing the article, he defined Stubs as:

A test stub looks like a real collaborating object (a real dependency), can be configured to return values or throw exceptions. It allows us to test code that interacts with the object replaced by a test stub without executing the code of the real dependency.

And, he defined Mocks as:

A mock object is a test stub that can be configured to expect messages (method calls). This makes it possible to test the communication between objects. A mock object is used as an observation point to verify the indirect outputs of the system under test (SUT) during its execution. A mock object also includes the functionality of a test stub, as it must return values to the SUT. However, the focus is on verifying the indirect outputs, i.e. the communication between the SUT and the object replaced by the mock object.

NOTE: The emphasis in the above quotes is mine.

I found this distinction (along with the remainder of the article) quite intriguing; even though I needed to read the article a few times to let it all sink in fully.

I hope that I have it clear in my head now, that being that with Stubs we're testing the SUT (System Under Test), ensuring that it's received the objects that it needs, without being concerned about those objects, just that it has received them. With Mocks, however, we want to verify the functionality between the System Under Test and the mock objects passed to it.

If you'd like a much deeper dive into the topic, check out Mocks Aren't Stubs by the venerable Martin Fowler.

From a lines of code perspective (as you can see in the code example a few paragraphs down) the only difference is a single function call. So by creating stubs instead of mocks, you're not going to save on file size or lines of code. However, from a readability and maintainability perspective, I agree that when using the appropriate call (createMock() or createStub()) tests become more revealing about their intent.

Specifically, when you create a stub, you're doing so only because a class or function requires it. You're not interested in what the stub does during the execution of the SUT. Whereas, with a mock, you are interested in verifying that its behaviour was correct.

I'm not sure why, but this distinction has escaped me up until now. So, I'm very appreciative of Sebastian's article in clarifying the difference for me. Also, after reading through the full article, I don't feel so bad, as Sebastian admits that his knowledge evolved slowly over time, just as mine has; even if his evolved quite a lot faster than mine has.

Regardless, to correct the above code required only a small change. Specifically, I only need to change the call to getMenu() as in the following code example:

$menu = $app->getMenu(
    $request,
    $this->createStub(ResponseInterface::class),
    ['step' => $step],
);

By doing this, it's clear that there are no expectations on the ResponceInterface stub. It's only there as it's required by getMenu().

That's been a brief look at the difference between bocks and stubs

I hope that you found this broad discussion of the differences between mocks and stubs helpful, and that, if you're planning to upgrade to PHPUnit 12.5 - or have already - that it will help you better understand some of the new errors that you'll see.