Mocking & Test Doubles
Real applications are full of dependencies a unit test doesn't want to deal with: a database connection, a third-party payment API, the current time, the filesystem. If a test for an order-processing function actually hits a payment provider's live API every time it runs, the test suite becomes slow, unreliable when the network hiccups, and potentially expensive if it triggers real charges. Mocking solves this by substituting a fake, controllable stand-in — a test double — for the real dependency, so the test can focus purely on the logic of the class under test.
Why isolate the unit under test
Consider a class that processes an order by charging a customer through a PaymentGateway and then saving the result with an OrderRepository. A unit test for OrderProcessor shouldn't need a live payment gateway or a real database — it should verify that OrderProcessor calls those collaborators correctly given various scenarios (successful charge, declined card, gateway timeout), regardless of how the real payment gateway happens to be implemented.
The class under test
<?php
class OrderProcessor
{
public function __construct(
private PaymentGateway $paymentGateway,
private OrderRepository $orderRepository,
) {
}
public function process(Order $order): bool
{
$charged = $this->paymentGateway->charge($order->total());
if (!$charged) {
return false;
}
$this->orderRepository->save($order);
return true;
}
}Because PaymentGateway and OrderRepository are injected through the constructor rather than created inside the class, a test can pass in fake versions instead. This is exactly why constructor injection is worth using even in small projects — it's what makes a class mockable in the first place.
createMock() and createStub()
PHPUnit can generate a fake implementation of any class or interface
at runtime with $this->createMock(). The generated object accepts
any method call the real class defines, returns null by default
from each one, and lets you configure specific return values or
expectations per test.
Using createMock() to fake a successful charge
<?php
use PHPUnit\Framework\TestCase;
class OrderProcessorTest extends TestCase
{
public function testProcessSavesOrderWhenChargeSucceeds(): void
{
$paymentGateway = $this->createMock(PaymentGateway::class);
$paymentGateway->method('charge')->willReturn(true);
$orderRepository = $this->createMock(OrderRepository::class);
$processor = new OrderProcessor($paymentGateway, $orderRepository);
$order = new Order(total: 49.99);
$result = $processor->process($order);
$this->assertTrue($result);
}
}createStub() is nearly identical but signals intent: a stub only supplies canned return values (like willReturn(true) above) and isn't meant to have call expectations asserted on it, while a mock is meant for verifying interactions actually happened. In practice PHPUnit's createMock() can do both, but reaching for createStub() when you only care about a return value makes the test's intent clearer to the next reader.
Setting expectations on how a mock is called
Beyond controlling return values, mocks can assert that a method was called a specific number of times, or with specific arguments. This is useful for verifying side effects that don't show up in a return value — for example, confirming the order was actually saved.
Asserting save() is called exactly once
<?php
public function testProcessSavesOrderExactlyOnce(): void
{
$paymentGateway = $this->createStub(PaymentGateway::class);
$paymentGateway->method('charge')->willReturn(true);
$orderRepository = $this->createMock(OrderRepository::class);
$orderRepository->expects($this->once())
->method('save')
->with($this->isInstanceOf(Order::class));
$processor = new OrderProcessor($paymentGateway, $orderRepository);
$processor->process(new Order(total: 49.99));
}expects($this->once()) says the test should fail if save() is
called zero times or more than once. ->with(...) further restricts
which arguments count as a matching call. This test has no explicit
assert* call at all — the mock's expectation is itself the
assertion, and PHPUnit checks it automatically when the test method
finishes.
Testing the declined-charge path
<?php
public function testProcessDoesNotSaveWhenChargeIsDeclined(): void
{
$paymentGateway = $this->createStub(PaymentGateway::class);
$paymentGateway->method('charge')->willReturn(false);
$orderRepository = $this->createMock(OrderRepository::class);
$orderRepository->expects($this->never())->method('save');
$processor = new OrderProcessor($paymentGateway, $orderRepository);
$result = $processor->process(new Order(total: 49.99));
$this->assertFalse($result);
}$this->never() is just as useful as $this->once() — confirming a
method was not called is often exactly the behavior you're trying
to protect, here making sure a declined payment never results in an
order being saved.
Mocks vs. real integration tests
Mocking is powerful, but it comes with a real tradeoff. A mock only behaves the way you told it to behave — it doesn't know anything about how the real PaymentGateway actually responds to edge cases, network errors, or a changed API contract. A test suite built entirely out of mocked collaborators can pass with flying colors while the real integration between those pieces is completely broken.
Mock or stub dependencies that are slow, external, or hard to control (databases, HTTP APIs, the clock, the filesystem).
Use
createStub()when you only need a canned return value; usecreateMock()when you need to assert a method was actually called.expects($this->once()),expects($this->never()), and->with(...)verify how a collaborator was used, not just what it returned.Balance mocked unit tests with a smaller set of real integration tests so a broken assumption about a dependency doesn't slip through unnoticed.