Skip to Content
Course content

70: Mocking with PHPUnit

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

Alright, let's look at a common headache I ran into early in my career. I was working on an order processing system, and every time I ran my test suite, it tried to send actual emails to customers. Not only was it slow, but I accidentally spammed a few test accounts because I forgot to change the environment variables. We can't have that.

The goal here is to test the OrderProcessor logic—making sure the order is marked as "paid" and the total is correct—without actually triggering the EmailService. In a perfect world, the OrderProcessor shouldn't even know how the email is sent; it just knows it needs to tell some service to do it.

The problem with real dependencies

Here is the setup I'm working with. I've got a simple EmailService that handles the heavy lifting of connecting to an SMTP server, and an OrderProcessor that uses it.

class EmailService {
    public function sendConfirmation(string $email, string $orderId): bool {
        // Imagine a lot of complex network code here
        echo "Sending real email to $email..."; 
        return true;
    }
}

class OrderProcessor {
    private $mailer;

    public function __construct(EmailService $mailer) {
        $this->mailer = $mailer;
    }

    public function completeOrder($order): void {
        $order->status = 'completed';
        $this->mailer->sendConfirmation($order->customerEmail, $order->id);
    }
}

Now, if I write a standard PHPUnit test and pass in a real EmailService, my test will actually try to "send" that email. If the SMTP server is down, my test fails. If the internet is slow, my tests crawl. That's a "fragile test," and we want to avoid that at all costs.

Swapping the real deal for a double

I don't want a real EmailService, but OrderProcessor requires one in its constructor. This is where mocking comes in. Instead of writing a whole new "FakeEmailService" class manually, I can tell PHPUnit to generate one for me on the fly using createMock().

Let's see what happens when I just create a basic mock without any instructions:

public function testCompleteOrderSendsEmail() {
    $mockMailer = $this->createMock(EmailService::class);
    $processor = new OrderProcessor($mockMailer);
    
    $order = (object)['id' => '123', 'customerEmail' => 'test@example.com', 'status' => 'pending'];
    $processor->completeOrder($order);
    
    $this->assertEquals('completed', $order->status);
}

This passes! But there's a problem. The test passes even if I delete the line $this->mailer->sendConfirmation(...) from the OrderProcessor class. Why? Because the mock object is just a hollow shell. It accepts any method call and returns null by default. It's not actually verifying that the email was sent; it's just staying out of the way.

Demanding a specific interaction

To make this test actually useful, I need to move from "just ignore the mailer" to "verify the mailer was used correctly." I want to ensure that sendConfirmation is called exactly once, and that it's called with the right email address.

I'll adjust the test to set an expectation:

public function testCompleteOrderSendsEmail() {
    $mockMailer = $this->createMock(EmailService::class);

    // Here is where the magic happens
    $mockMailer->expects($this->once()) 
               ->method('sendConfirmation')
               ->with('test@example.com', '123')
               ->willReturn(true);

    $processor = new OrderProcessor($mockMailer);
    $order = (object)['id' => '123', 'customerEmail' => 'test@example.com', 'status' => 'pending'];
    
    $processor->completeOrder($order);
}

Now, the test is much stricter. If I comment out the mailer call in the OrderProcessor, PHPUnit will throw an exception: "Expectation failed fulfilling specified invocation count." It's basically saying, "You promised me this method would be called once, but it never happened."

Handling different return values

Sometimes we need to test how our code handles failure. What if the email service returns false? If my OrderProcessor had logic to log a failure or throw an exception when the email fails, I could test that simply by changing the mock's behavior.

I just change ->willReturn(true) to ->willReturn(false). I don't have to manually break my SMTP server to see if my error handling works; I just tell the mock to lie to the OrderProcessor. This is the real power of mocking: you control the universe your class lives in during the test.




📋 Practical Task

Exercise: Verifying the Payment Gateway Integration

You have a PaymentProcessor class that depends on a PaymentGateway. You need to write a test that ensures the PaymentGateway's charge() method is called exactly once with the correct amount and currency, and that the PaymentProcessor returns true when the gateway confirms the charge.

The classes are already provided:

class PaymentGateway {
    public function charge(float $amount, string $currency): bool {
        // Connects to an external API (Stripe, PayPal, etc.)
        return true; 
    }
}

class PaymentProcessor {
    private $gateway;

    public function __construct(PaymentGateway $gateway) {
        $this->gateway = $gateway;
    }

    public function processPayment(float $amount): bool {
        // We assume USD for this simple processor
        return $this->gateway->charge($amount, 'USD');
    }
}

Your Task: Create a PHPUnit test method that mocks the PaymentGateway, verifies the charge method is called with (99.99, 'USD'), and asserts that processPayment(99.99) returns true.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.