Mocking, Stubbing, Spying, and Faking in PHP: A Practical Guide (with Sandbox Example)

Modern PHP applications rely on many external components: APIs, databases, file systems, random generators, time providers, and services that communicate with the outside world.
When writing tests, you rarely want to interact with these real services. Doing so would make your tests slow, unpredictable, and difficult to run in isolation.

This is where test doubles come into play.

A test double is any stand-in object that replaces a real dependency during testing. PHP offers many ways to create these objects — manually, through libraries like Mockery, or through PHPUnit’s built-in mocking tools.

In this article, you’ll learn:

  • What mocks, stubs, spies, and fakes really are
  • How they differ and when to use each
  • How to build them in pure PHP
  • How to test code that depends on external services
  • Common mistakes developers make when mocking
  • Simple examples you can run directly in onlinephp.io

Let’s begin with the basics.

Previous Article: https://codecraftdiary.com/2025/11/13/writing-maintainable-feature-test-laravel-example/

1. The Four Main Types of Test Doubles

There are different categories of test doubles. Each serves a very specific purpose.

A stub returns predefined values.
It does not care how it is called — it simply responds.

A mock expects specific calls (method names, arguments).
If the expected call doesn’t happen, the test fails.

A fake is a lightweight alternative implementation.
It behaves similarly to the real object, but in a simplified way.

A spy records method calls for later inspection.
Unlike mocks, spies don’t enforce expectations up front.

Understanding which one you need dramatically improves test clarity.

This sandbox demonstrates all four main types of test doubles in PHP:

  • Stub – returns a fixed, deterministic value without checking how it’s called
  • Mock – verifies that a method was called with the expected parameters
  • Fake – provides a lightweight in-memory implementation that simulates realistic behavior
  • Spy – records method calls for later inspection

You can run this example directly in your browser: Open in OnlinePHP.io

<?php
// -------------------------------------
// Interface
// -------------------------------------
interface PaymentGateway {
    public function charge(int $amount): bool;
}

// -------------------------------------
// 1) Stub: always returns success
// -------------------------------------
class PaymentGatewayStub implements PaymentGateway {
    public function charge(int $amount): bool {
        return true; // Always succeed
    }
}

// -------------------------------------
// 2) Mock: tracks if method was called with expected amount
// -------------------------------------
class PaymentGatewayMock implements PaymentGateway {
    public bool $called = false;
    public ?int $chargedAmount = null;
    private int $expectedAmount;

    public function __construct(int $expectedAmount) {
        $this->expectedAmount = $expectedAmount;
    }

    public function charge(int $amount): bool {
        $this->called = true;
        $this->chargedAmount = $amount;
        return true;
    }

    public function verify(): bool {
        return $this->called && $this->chargedAmount === $this->expectedAmount;
    }
}

// -------------------------------------
// 3) Fake: stores transactions in memory
// -------------------------------------
class PaymentGatewayFake implements PaymentGateway {
    public array $transactions = [];

    public function charge(int $amount): bool {
        $this->transactions[] = [
            'amount' => $amount,
            'time' => date('H:i:s'),
        ];
        return true;
    }
}

// -------------------------------------
// 4) Spy: records method calls for later inspection
// -------------------------------------
class PaymentGatewaySpy implements PaymentGateway {
    public array $calls = [];

    public function charge(int $amount): bool {
        $this->calls[] = $amount;
        return true;
    }
}

// -------------------------------------
// Function under test
// -------------------------------------
function processOrder(PaymentGateway $gateway, int $amount): bool {
    return $gateway->charge($amount);
}

// -------------------------------------
// Run examples
// -------------------------------------

echo "=== STUB ===\n";
$stub = new PaymentGatewayStub();
echo processOrder($stub, 1000) ? "Order OK\n\n" : "Order FAIL\n\n";

echo "=== MOCK ===\n";
$mock = new PaymentGatewayMock(500);
processOrder($mock, 500);
echo $mock->verify() ? "Mock expectations met!\n\n" : "Mock expectations NOT met!\n\n";

echo "=== FAKE ===\n";
$fake = new PaymentGatewayFake();
$fake->charge(300);
$fake->charge(400);
echo "Fake stored " . count($fake->transactions) . " transactions:\n";
print_r($fake->transactions);
echo "\n";

echo "=== SPY ===\n";
$spy = new PaymentGatewaySpy();
$spy->charge(100);
$spy->charge(200);
echo "Spy recorded calls:\n";
print_r($spy->calls);
PHP

Understanding the Four Types of Test Doubles in Depth

While the short definitions give you an overview, let’s dig deeper into each type to understand their purpose and when to use them.

A stub is a simple object that provides predefined responses to method calls. It doesn’t care how the system under test uses it — its only job is to return consistent data.

Use cases:

  • Replace slow or unreliable dependencies (like APIs or databases) with deterministic responses.
  • Test logic that depends on specific return values without triggering side effects.

Example scenario:
You want to test a payment processing method, but you don’t want to actually charge a credit card. A stub can return true for all charges, allowing your test to focus on the processing logic itself.

A mock is more strict than a stub. It expects certain interactions, such as specific methods being called with exact arguments. If those expectations aren’t met, the test fails.

Use cases:

  • Verify that your code interacts correctly with external services.
  • Assert side effects, such as ensuring an email is sent or a log is recorded.

Example scenario:
You need to check that charge() was called with an exact amount. If your system calls it with a different amount or doesn’t call it at all, the mock will fail the test.

A fake is a fully functional, but simplified version of a dependency. Unlike stubs, fakes implement actual logic, often in memory, without hitting real external systems.

Use cases:

  • Simulate complex behavior without using production resources.
  • Run tests that need realistic interactions but shouldn’t rely on slow or fragile infrastructure.

Example scenario:
You create an in-memory payment gateway that stores transactions. Your application code can charge multiple times, inspect the transaction history, and run full workflows — all without connecting to a real payment system.

A spy is a special object that records information about how it was used during the test. Unlike mocks, it doesn’t enforce expectations up front. Instead, you can inspect the recorded calls afterward to assert behavior.

Use cases:

  • Monitor method calls and arguments for verification.
  • Capture additional details about interactions without failing the test immediately.

Example scenario:
You want to verify that charge() was called twice and with specific amounts. The spy keeps track of all calls so you can make assertions afterward, making it ideal for post-test verification.

Conclusion

Mocking and faking are essential techniques in modern PHP testing. When used correctly, they allow you to:

  • test complex code in isolation
  • avoid slow and unreliable external calls
  • write fast and deterministic tests
  • simulate edge cases that are impossible to trigger in real systems

Each test double has a clear purpose:

TypePurpose
StubReturn predetermined data
MockEnforce call expectations
FakeLightweight working implementation
SpyRecord calls for later inspection

By combining these techniques, you can write cleaner, safer, and more maintainable tests — whether you use PHPUnit, Pest, Mockery, or no framework at all.

Leave a Reply

Your email address will not be published. Required fields are marked *