0

Mock — a test double with behaviour expectations that will be verified

Beginner5 min read·eng-17-005
interview

Concept

Mock — a test double that has behavior expectations set IN ADVANCE. After the test runs, the mock verifies that specific methods were called with specific arguments. If expectations aren't met, the test fails.

Mock ≠ "any test double": In common usage, "mock" is used loosely for any test double. In the precise xUnit terminology, a mock is specifically one with pre-set expectations that are verified automatically.

Two things a mock does:

  1. Stubs behavior: willReturn(), willThrowException() — defines what the method returns.
  2. Verifies behavior: expects($this->once()), with($this->equalTo('expected')) — asserts the method was called correctly.

When to use mocks:

  • Testing that a notification was sent.
  • Testing that a repository's save() was called once.
  • Testing that an event was dispatched.
  • Testing that an external service was called with correct arguments.

PHPUnit mock API:

  • $this->createMock(ClassName::class): Create a mock of a class/interface.
  • ->expects($this->once()): Expect the method to be called exactly once.
  • ->method('methodName'): Which method.
  • ->with($arg1, $arg2): With these arguments.
  • ->willReturn($value): And it returns this.

Matchers: $this->once(), $this->exactly(3), $this->atLeastOnce(), $this->never(), $this->any().

Argument matchers: $this->equalTo($val), $this->isInstanceOf(Class::class), $this->anything(), $this->stringContains('text').

Mock vs Spy: Mocks set expectations BEFORE the test runs. Spies let the code run and THEN you assert calls happened. PHPUnit mocks can do both.

Code Example

php
<?php
class NotificationServiceTest extends \PHPUnit\Framework\TestCase
{
    public function test_sends_email_when_order_placed(): void
    {
        // CREATE MOCK with expectations
        $mockMailer = $this->createMock(\Illuminate\Contracts\Mail\Mailer::class);

        // SET EXPECTATIONS BEFORE running the code
        $mockMailer
            ->expects($this->once())           // must be called exactly once
            ->method('to')                      // calling the 'to' method
            ->with('alice@example.com')         // with this argument
            ->willReturn($mockMailer);           // fluent — returns itself

        $mockMailer
            ->expects($this->once())
            ->method('send')
            ->with($this->isInstanceOf(\App\Mail\OrderConfirmation::class)); // matches any OrderConfirmation instance

        // RUN THE CODE
        $service = new NotificationService($mockMailer);
        $service->notifyOrderPlaced(
            new Order(['id' => 1, 'user_email' => 'alice@example.com'])
        );

        // PHPUnit automatically verifies expectations at the end of the test
        // If 'send' wasn't called → test fails with: Expected 'send' to be called 1 time(s), called 0 time(s)
    }

    public function test_does_not_send_email_when_order_cancelled(): void
    {
        $mockMailer = $this->createMock(\Illuminate\Contracts\Mail\Mailer::class);

        // Expect send() is NEVER called for cancelled orders
        $mockMailer->expects($this->never())->method('send');

        $service = new NotificationService($mockMailer);
        $service->notifyOrderCancelled(new Order(['id' => 1, 'status' => 'cancelled']));
    }

    public function test_sends_to_multiple_recipients(): void
    {
        $mockMailer = $this->createMock(\Illuminate\Contracts\Mail\Mailer::class);

        // Expect send() to be called 3 times (3 recipients)
        $mockMailer->expects($this->exactly(3))->method('send');

        $service = new NotificationService($mockMailer);
        $service->broadcastOrderUpdate(new Order(['id' => 1]), ['a@x.com', 'b@x.com', 'c@x.com']);
    }

    public function test_logs_on_failure(): void
    {
        $mockMailer = $this->createMock(\Illuminate\Contracts\Mail\Mailer::class);
        $mockLogger = $this->createMock(\Psr\Log\LoggerInterface::class);

        // Stub: mailer throws exception
        $mockMailer->method('send')->willThrowException(new \RuntimeException('SMTP error'));

        // Expect: logger records the error
        $mockLogger->expects($this->once())
                   ->method('error')
                   ->with($this->stringContains('SMTP error'));

        $service = new NotificationService($mockMailer, $mockLogger);
        $service->notifyOrderPlaced(new Order(['id' => 1, 'user_email' => 'alice@x.com']));
        // No exception propagated — service caught it and logged it
    }
}