Skip to Content
Course content

166: Mocking Objects with PHPUnit Mock Builder

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring pattern when I review code from developers moving into professional TDD: they treat mocking as a way to simply "get the code to run" without crashing. They often create a whole directory of "Fake" classes—like FakePaymentGateway or MockUserRepository—manually implementing interfaces just to return a hardcoded value.

Thinking Manual Fakes are Easier than Mocks

When you manually create a fake class, you're essentially writing more production code just to test your production code. Let's say you have a PaymentGateway interface. You might write a FakePaymentGateway that always returns true for a charge() method. It works, but it's brittle. If you add a new parameter to the charge() method in the interface, your test suite suddenly breaks in ten different "Fake" classes. You've just doubled your maintenance burden.

// The "Manual Fake" trap - avoid this for simple behavior
class FakePaymentGateway implements PaymentGateway {
    public function charge($amount) {
        return true; // Hardcoded and inflexible
    }
}

Using the Mock Builder to Verify Interactions

The real power of the PHPUnit Mock Builder isn't just providing a return value; it's behavior verification. I don't just want to know that my OrderProcessor returned true; I want to know that it actually called the charge() method exactly once, and that it passed the correct dollar amount.

Here is how we do this using the Mock Builder. Instead of a separate class, we generate the object on the fly:

public function testOrderProcessesPaymentCorrectly()
{
    // 1. Create the mock
    $paymentMock = $this->createMock(PaymentGateway::class);

    // 2. Set the expectations
    $paymentMock->expects($this->once()) // I expect this to be called exactly once
                ->method('charge')       // This specific method
                ->with(99.99)            // With this exact argument
                ->willReturn(true);      // And it should return this value

    $processor = new OrderProcessor($paymentMock);
    $result = $processor->process(99.99);

    $this->assertTrue($result);
}

Notice the shift in logic. We aren't just creating a "dumb" object; we are defining a contract for this specific test. If the OrderProcessor accidentally calls charge() twice, or calls it with 0.00, PHPUnit will fail the test immediately, even if the final result is true. This is where you catch the subtle bugs that manual fakes miss.

Handling Complex Return Sequences

Sometimes, a single return value isn't enough. You might have a loop that checks a status multiple times. I used to struggle with this until I discovered willReturnOnConsecutiveCalls(). It's a lifesaver when you're testing polling mechanisms or retry logic.

$paymentMock->method('getStatus')
              ->willReturnOnConsecutiveCalls('pending', 'pending', 'completed');

In this scenario, the first two times your code calls getStatus(), it gets 'pending'. The third time, it gets 'completed'. This allows you to test that your loop actually iterates and doesn't just exit early or hang forever. It's clean, it's inline, and you didn't have to write a single extra class file to achieve it.




📋 Practical Task

Exercise: Testing the NotificationDispatcher with a Mocked SMS Gateway

You have a NotificationDispatcher class that depends on an SmsGatewayInterface. Your goal is to ensure that the dispatcher only sends an SMS if the user has opted-in to notifications.

The Requirements:

  • Create a test case where a user has optIn = true. Verify that the sendSms() method of the SmsGatewayInterface is called exactly once with the correct phone number.
  • Create a second test case where a user has optIn = false. Verify that the sendSms() method is never called.

Starter Code:

interface SmsGatewayInterface {
    public function sendSms(string $number, string $message): bool;
}

class NotificationDispatcher {
    private $gateway;
    public function __construct(SmsGatewayInterface $gateway) {
        $this->gateway = $gateway;
    }

    public function dispatch(User $user, string $msg) {
        if ($user->isOptedIn()) {
            return $this->gateway->sendSms($user->getPhone(), $msg);
        }
        return false;
    }
}

Implement the PHPUnit test class using createMock(), expects(), and with() to validate these two behaviors.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.