Mocking Objects

A real application is full of classes that talk to the outside world — a class that saves an order to a database, one that calls a payment API, one that reads a file from disk. Testing the logic around those calls shouldn't require an actual database connection, a real HTTP request, or real money moving through a payment gateway every time you run your test suite. A mock is a fake stand-in object that lets you test a class in isolation from its real dependencies.

What Mocking Is For

Suppose an Order class depends on a PaymentGatewayInterface to charge a customer's card. If your test used the real payment gateway, every single test run would attempt an actual charge — slow, unreliable (what if the network is down?), and potentially costing real money. Mocking replaces that dependency with a fake object that behaves exactly the way you tell it to, so the test only exercises the Order class's own logic.

Without a mockWith a mock
Test hits a real database or APITest uses a fake object that returns exactly what you specify
Slow, and fails if the external service is downFast and runs the same every time, anywhere
Hard to test edge cases (e.g. "what if the API errors?")Trivial to simulate any return value or exception

Creating a Mock with createMock()

Inside any test method, $this->createMock(SomeInterface::class) builds a fake object that implements the given interface (or extends the given class), with every method automatically stubbed to return null until you configure it otherwise.

<?php
$mock = $this->createMock(PaymentGatewayInterface::class);

Configuring Return Values

Use method() together with willReturn() to tell the mock exactly what to hand back when a particular method is called — no real lookup ever happens.

<?php
$fakeUser = new User(id: 1, name: 'Jane Doe');

$mock = $this->createMock(UserRepositoryInterface::class);
$mock->method('getUser')
    ->willReturn($fakeUser);

// Any code that calls $mock->getUser(...) now gets $fakeUser back, instantly

Verifying a Method Was Called

Sometimes what matters isn't a return value but whether a method was called at all — and how many times. expects() combined with method() turns the mock into an assertion: the test fails if the expectation isn't met by the time the test finishes.

<?php
$mock->expects($this->once())
    ->method('save');

// If ->save() is never called, or called more than once, the test fails

Worked Example: Mocking a Payment Gateway

Here's the motivating example in full — an Order class that should call charge() on its payment gateway exactly once, with the order's total. The test mocks PaymentGatewayInterface so nothing ever touches a real payment API.

<?php
use PHPUnit\Framework\TestCase;

interface PaymentGatewayInterface
{
    public function charge(float $amount): bool;
}

class Order
{
    public function __construct(
        private PaymentGatewayInterface $gateway,
        private float $total
    ) {}

    public function checkout(): bool
    {
        return $this->gateway->charge($this->total);
    }
}

class OrderTest extends TestCase
{
    public function testCheckoutChargesTheGatewayForTheOrderTotal()
    {
        $mockGateway = $this->createMock(PaymentGatewayInterface::class);

        $mockGateway->expects($this->once())
            ->method('charge')
            ->with(49.99)
            ->willReturn(true);

        $order = new Order($mockGateway, 49.99);

        $this->assertTrue($order->checkout());
    }
}

This test proves the Order class calls charge() with the right amount, exactly once — without ever creating a real PaymentGatewayInterface implementation or contacting a real payment provider.

Unit Tests vs. Integration Tests

The example above is a unit test: it isolates one class (Order) by replacing its dependency with a mock, so only Order's own logic is under test. An integration test, by contrast, uses the real dependency — a real database, a real (sandboxed) API — to verify that the pieces actually work together correctly. Both have their place: unit tests are fast and pinpoint exactly which class broke; integration tests catch problems that only show up when real components interact. This page focuses on the unit-testing side, since that's where mocking does its work.

Note: Mocking every dependency doesn't remove the need for integration tests entirely — a suite of only heavily-mocked unit tests can still miss a bug in how two real components actually wire together. Most projects want a mix of both.
Next up: Code Coverage covers measuring how much of your source code your test suite actually exercises.