Mocking #
Mocking is a testing technique that replaces real dependencies with fake objects whose behavior can be controlled — so the unit under test can be tested in isolation without involving databases, networks, or external services. The previous article (Unit Test) introduced basic PHPUnit stubs and mocks. This article covers mocking in more depth: the complete test double taxonomy, the subtle differences between stubs, mocks, spies, and fakes, more complex mocking techniques, Mockery as a more expressive alternative, and — most importantly — when mocking is actually a sign that the code design needs reconsideration.
The Test Double Taxonomy #
“Mock” is often used as a generic term, even though there are five different types of test doubles, each with its own role:
flowchart TD
TD[Test Double] --> Dummy
TD --> Stub
TD --> Spy
TD --> Mock
TD --> Fake
Dummy --> D1["Fills a required\nparameter not\nused in the test"]
Stub --> S1["Returns pre-programmed\nvalues\nwithout verification"]
Spy --> SP1["Records how it\nwas called,\nqueryable afterward"]
Mock --> M1["Verifies expectations\nautomatically\nwhen the test ends"]
Fake --> F1["Simple but genuinely\nworking\nimplementation"]
style Stub fill:#dcfce7
style Mock fill:#dbeafe
style Fake fill:#fef9c3<?php
use PHPUnit\Framework\TestCase;
// 1. DUMMY — fills a parameter but is never called or used
class DummyLogger implements LoggerInterface
{
public function log(string $msg): void {} // does nothing
public function info(string $msg): void {}
public function error(string $msg): void {}
}
// Used when the tested class needs a logger but the test doesn't care about logging
$service = new OrderService(new DummyLogger(), $repo);
// 2. STUB — returns pre-programmed values
$stub = $this->createStub(UserRepository::class);
$stub->method('findById')->willReturn(new User(id: 1, name: 'Budi'));
// Doesn't verify how many times it's called or with what arguments
// 3. MOCK — verifies interaction expectations
$mock = $this->createMock(MailerInterface::class);
$mock->expects($this->once())->method('send'); // MUST be called exactly once
// Verification happens automatically when the test ends
// 4. SPY — records calls, verifies after the fact
// PHPUnit doesn't have a built-in spy, but it can be simulated
$spy = $this->createMock(EventDispatcher::class);
$spy->method('dispatch')
->willReturnCallback(function($event) use (&$dispatchedEvents) {
$dispatchedEvents[] = $event;
return true;
});
// Execute
$this->service->run();
// Verify after the fact (spy style)
$this->assertCount(2, $dispatchedEvents);
$this->assertInstanceOf(OrderCreatedEvent::class, $dispatchedEvents[0]);
// 5. FAKE — a simple implementation that genuinely works
class InMemoryUserRepository implements UserRepositoryInterface
{
private array $users = [];
public function save(User $user): void
{
$this->users[$user->getId()] = $user;
}
public function findById(int $id): ?User
{
return $this->users[$id] ?? null;
}
public function findAll(): array
{
return array_values($this->users);
}
}
// A fake is far more realistic than a stub — usable across many tests
$fakeRepo = new InMemoryUserRepository();
$fakeRepo->save(new User(id: 1, name: 'Budi'));
$service = new OrderService($fakeRepo);
Advanced PHPUnit Mocking #
Matching Arguments with Constraints #
PHPUnit provides various constraints for verifying the arguments sent to a mock:
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\{
IsEqual, IsInstanceOf, StringContains,
IsType, LogicalAnd, Anything
};
class EmailServiceTest extends TestCase
{
public function sendsOrderConfirmationEmail(): void
{
$mailerMock = $this->createMock(MailerInterface::class);
$mailerMock->expects($this->once())
->method('send')
->with(
// Argument 1: email — must be in email format
$this->matchesRegularExpression('/^[\w.]+@[\w.]+\.[a-z]{2,}$/'),
// Argument 2: subject — must contain the order number
$this->logicalAnd(
$this->stringContains('Order'),
$this->stringContains('#42')
),
// Argument 3: email body — must contain specific information
$this->callback(function(string $body): bool {
return str_contains($body, 'Rp 150.000')
&& str_contains($body, 'Budi Santoso');
}),
)
->willReturn(true);
$service = new EmailService($mailerMock);
$service->sendOrderConfirmation(new Order(
id: 42,
email: '[email protected]',
total: 150_000,
customerName: 'Budi Santoso',
));
}
}
Mocks with Callbacks #
When a mock’s behavior needs to be more complex than a static return value, use willReturnCallback:
<?php
$cacheMock = $this->createMock(CacheInterface::class);
// Simulate a cache that genuinely stores and retrieves values
$cacheStore = [];
$cacheMock->method('get')
->willReturnCallback(function(string $key) use (&$cacheStore) {
return $cacheStore[$key] ?? null;
});
$cacheMock->method('set')
->willReturnCallback(function(string $key, mixed $value, int $ttl) use (&$cacheStore) {
$cacheStore[$key] = $value;
return true;
});
$cacheMock->method('delete')
->willReturnCallback(function(string $key) use (&$cacheStore) {
unset($cacheStore[$key]);
return true;
});
// Now the mock behaves like a real cache
// More useful for complex tests
Ordering Expectations #
If the call order matters, use InvokedAtIndex or InvokedInSequence:
<?php
$dbMock = $this->createMock(DatabaseInterface::class);
// Verify the order: begin → query → commit
$dbMock->expects($this->at(0))->method('begin');
$dbMock->expects($this->at(1))->method('query')
->with($this->stringContains('INSERT'));
$dbMock->expects($this->at(2))->method('commit');
// Or with getMockBuilder for more configuration
$loggerMock = $this->getMockBuilder(LoggerInterface::class)
->disableOriginalConstructor() // don't call __construct()
->onlyMethods(['log', 'error']) // only mock these methods
->getMock();
$loggerMock->expects($this->exactly(2))
->method('log')
->withConsecutive(
['info', 'Process started'], // first call
['info', 'Process finished'], // second call
);
Partial Mocks #
Partial mocks allow overriding only specific methods of a real class — other methods keep their original implementation:
<?php
class ReportService
{
public function generate(array $data): string
{
$processed = $this->processData($data); // the method to mock
$formatted = $this->formatOutput($processed); // the method that stays real
return $formatted;
}
protected function processData(array $data): array
{
// Heavy operation — database access, complex calculations
sleep(5); // simulate slowness
return $data;
}
protected function formatOutput(array $data): string
{
return json_encode($data); // lightweight, keep the original
}
}
class ReportServiceTest extends TestCase
{
public function producesReportWithCorrectFormat(): void
{
// Partial mock — mock only processData, formatOutput stays real
$service = $this->getMockBuilder(ReportService::class)
->onlyMethods(['processData']) // only this method is mocked
->getMock();
$service->method('processData')
->willReturn(['id' => 1, 'name' => 'Test']); // fast, no sleep
$result = $service->generate(['id' => 1]);
// The real formatOutput() is called — can verify actual output
$this->assertSame('{"id":1,"name":"Test"}', $result);
}
}
Mockery — A More Expressive Alternative #
Mockery is an alternative mocking library often considered more readable and more flexible than PHPUnit’s built-in mocking system:
composer require --dev mockery/mockery
Mockery Syntax #
<?php
use Mockery;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;
class OrderServiceMockeryTest extends TestCase
{
protected function tearDown(): void
{
Mockery::close(); // REQUIRED — cleans up Mockery expectations
parent::tearDown();
}
public function calculatesOrderTotal(): void
{
// Create a mock with Mockery
$repo = Mockery::mock(UserRepository::class);
// Mockery syntax: ->shouldReceive()->with()->andReturn()
$repo->shouldReceive('findById')
->once()
->with(42)
->andReturn(new User(id: 42, discount: 0.10));
$service = new OrderService($repo);
$total = $service->calculateTotalForUser(userId: 42, price: 100_000);
$this->assertSame(90_000.0, $total);
}
public function verifiesEmailSent(): void
{
$mailer = Mockery::mock(MailerInterface::class);
$mailer->shouldReceive('send')
->once()
->with(
Mockery::type('string'), // argument 1: any string
Mockery::pattern('/^Order #\d+/'), // argument 2: regex pattern
Mockery::any() // argument 3: anything
)
->andReturn(true);
$service = new NotifService($mailer);
$service->notifyOrder(new Order(id: 99, email: '[email protected]'));
}
public function unexpectedMock(): void
{
$cache = Mockery::mock(CacheInterface::class);
// Make sure this method is NOT called at all
$cache->shouldNotReceive('delete');
// Can be called any number of times (including zero)
$cache->shouldReceive('get')->zeroOrMoreTimes()->andReturn(null);
// Must be called at least once
$cache->shouldReceive('set')->atLeast()->once();
$service = new DataService($cache);
$service->loadData('key', fn() => 'value');
}
}
Mockery for Interfaces and Final Classes #
PHPUnit can’t mock final classes directly. Mockery has a solution via aliasing:
<?php
// A final class can't be extended/mocked normally
final class SmsGateway
{
public function send(string $phoneNumber, string $message): bool
{
// calls an external API
}
}
// Mockery alias — create a mock as if the class were mockable
$smsMock = Mockery::mock('overload:' . SmsGateway::class);
$smsMock->shouldReceive('send')->once()->andReturn(true);
// Or use a cleaner approach:
// Create an interface and wrapper, then mock the interface
interface SmsGatewayInterface
{
public function send(string $phoneNumber, string $message): bool;
}
class SmsGatewayWrapper implements SmsGatewayInterface
{
public function __construct(private SmsGateway $gateway) {}
public function send(string $phoneNumber, string $message): bool
{
return $this->gateway->send($phoneNumber, $message);
}
}
// Now the interface can be mocked with regular PHPUnit
$smsMock = $this->createMock(SmsGatewayInterface::class);
$smsMock->expects($this->once())->method('send')->willReturn(true);
Mocking Static Methods #
Static methods are hard to mock because they can’t be replaced via dependency injection. There are several strategies:
<?php
// Approach 1: Wrap in a class with an interface (best)
class TimeService
{
public function now(): \DateTimeImmutable
{
return new \DateTimeImmutable();
}
}
// Now TimeService can be injected and mocked
class OrderService
{
public function __construct(private TimeService $time) {}
public function createOrder(array $data): Order
{
return new Order(
...$data,
createdAt: $this->time->now(), // doesn't use static time()
);
}
}
class OrderServiceTest extends TestCase
{
public function orderHasTheCorrectDate(): void
{
$fixedTime = new \DateTimeImmutable('2024-03-15 14:00:00');
$timeStub = $this->createStub(TimeService::class);
$timeStub->method('now')->willReturn($fixedTime);
$service = new OrderService($timeStub);
$order = $service->createOrder(['user_id' => 1]);
$this->assertEquals($fixedTime, $order->getCreatedAt());
}
}
// Approach 2: Use a Clock abstraction (PSR-20 ClockInterface)
use Psr\Clock\ClockInterface;
class FakeClock implements ClockInterface
{
public function __construct(private \DateTimeImmutable $time) {}
public function now(): \DateTimeImmutable
{
return $this->time;
}
public function advance(string $interval): static
{
return new static($this->time->modify($interval));
}
}
$fakeClock = new FakeClock(new \DateTimeImmutable('2024-03-15'));
$service = new OrderService($fakeClock);
// Can "advance" time in the test!
$fakeClock = $fakeClock->advance('+1 day');
Fake vs Mock — Choosing the Right One #
For dependencies used frequently across many tests, a fake is better than a mock:
<?php
// Fake repository — a genuinely working in-memory implementation
class FakeUserRepository implements UserRepositoryInterface
{
private array $users = [];
private int $nextId = 1;
public function save(User $user): User
{
if ($user->getId() === null) {
$user = $user->withId($this->nextId++);
}
$this->users[$user->getId()] = $user;
return $user;
}
public function findById(int $id): ?User
{
return $this->users[$id] ?? null;
}
public function findByEmail(string $email): ?User
{
foreach ($this->users as $user) {
if ($user->getEmail() === $email) {
return $user;
}
}
return null;
}
public function findAll(): array
{
return array_values($this->users);
}
public function delete(int $id): void
{
unset($this->users[$id]);
}
// Helper methods for tests — not in the real interface
public function reset(): void
{
$this->users = [];
$this->nextId = 1;
}
public function count(): int
{
return count($this->users);
}
}
// Use it across many tests
class UserServiceTest extends TestCase
{
private FakeUserRepository $repo;
private UserService $service;
protected function setUp(): void
{
$this->repo = new FakeUserRepository();
$this->service = new UserService($this->repo);
}
public function savesNewUser(): void
{
$user = $this->service->register('Budi', '[email protected]');
$this->assertNotNull($user->getId());
$this->assertSame(1, $this->repo->count());
}
public function findsUserByEmail(): void
{
$this->repo->save(new User(name: 'Budi', email: '[email protected]'));
$found = $this->service->findByEmail('[email protected]');
$this->assertNotNull($found);
$this->assertSame('Budi', $found->getName());
}
}
Mocking Anti-Patterns to Avoid #
<?php
// ✗ Anti-pattern 1: Mocking all dependencies (over-mocking)
// If this test fails, it's hard to tell whether the SUT or the mock is wrong
public function testProcessOrder(): void
{
$repoMock = $this->createMock(OrderRepository::class);
$mailerMock = $this->createMock(MailerInterface::class);
$loggerMock = $this->createMock(LoggerInterface::class);
$cacheMock = $this->createMock(CacheInterface::class);
$eventMock = $this->createMock(EventDispatcher::class);
$validMock = $this->createMock(ValidatorInterface::class);
// 6 mocks? Maybe OrderService is doing too much
// A sign: the tested class has too many dependencies
}
// ✓ Consider refactoring — split responsibilities
// OrderService should only need 1-2 genuinely necessary dependencies
// ✗ Anti-pattern 2: Mocking concrete classes without an interface
$userMock = $this->createMock(User::class); // Don't mock Entities/Value Objects
// Create a real User with the values the test needs
$user = new User(id: 1, name: 'Test', email: '[email protected]');
// ✗ Anti-pattern 3: Mocks returning mocks
$repoMock = $this->createMock(UserRepository::class);
$userMock = $this->createMock(User::class); // STOP! this is a sign of trouble
$userMock->method('getName')->willReturn('Budi'); // just create a real User
$repoMock->method('findById')->willReturn($userMock);
// ✓ Return real objects from stubs/mocks
$repoMock->method('findById')->willReturn(
new User(id: 1, name: 'Budi', email: '[email protected]')
);
// ✗ Anti-pattern 4: Tests that only verify internal implementation
public function testRedisImplementation(): void
{
$redisMock = $this->createMock(\Redis::class);
// Verify that SET is called with a specific TTL
$redisMock->expects($this->once())
->method('setex')
->with('user:1', 3600, Mockery::any());
// This test fails if we switch Redis to Memcached
// even though the cache behavior stays the same!
}
// ✓ Test behavior observable from the outside
public function testUserStoredInCache(): void
{
$fakeCache = new InMemoryCache(); // a flexible fake
$service = new UserService($fakeCache);
$user1 = $service->getUser(1); // first call
$user2 = $service->getUser(1); // should come from cache
$this->assertEquals($user1, $user2);
$this->assertTrue($fakeCache->has('user:1'));
}
// ✗ Anti-pattern 5: Forgetting Mockery tearDown
public function testUsingMockery(): void
{
$mock = Mockery::mock(SomeClass::class);
// ... test
// If tearDown doesn't call Mockery::close(), expectations aren't verified!
}
// ✓ Always call Mockery::close() in tearDown
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
When Not to Mock #
Excessive mocking is a code smell — a sign the design needs reconsideration:
Don't mock:
✗ Value Objects and Entities (User, Order, Money) — create real instances
✗ Very simple classes without side effects (calculations, formatting)
✗ Fast, deterministic dependencies (in-memory collections, fixed dates)
✗ Every dependency in one test (a sign of a God Object or too much coupling)
Mock when:
✓ The dependency has side effects (sending emails, writing to a database, calling APIs)
✓ The dependency is slow (network calls, heavy queries)
✓ The dependency is non-deterministic (random, current time, UUIDs)
✓ You want to verify interactions — not just the final result
If you need many mocks for one test:
→ Consider splitting the tested class into several smaller classes
→ Use fakes (in-memory implementations) rather than mocks for complex dependencies
→ Write an integration test instead of forcing a unit test with many mocks
Summary #
- Five types of test doubles: Dummy (fills an unused parameter), Stub (returns pre-programmed values), Spy (records calls for later verification), Mock (verifies expectations automatically), Fake (a simple but genuinely working implementation).
- Stubs for data, Mocks for interactions — stubs return the values the SUT needs to run; mocks verify that the SUT interacts with dependencies correctly.
- A fake is better than a mock for dependencies used across many tests — an
InMemoryUserRepositoryis far easier to maintain and more realistic than dozens of reconfigured mocks.- Mockery provides more expressive syntax:
shouldReceive()->once()->with()->andReturn(). You must callMockery::close()intearDown().- Don’t mock final classes or static methods directly — wrap them in an interface/wrapper to make them testable, or use a Mockery alias (as a last resort).
- Partial mocks are useful for overriding one method of a real class when the original implementation is needed for the other methods.
- Signs of over-mocking: a test needs 5+ mocks; mocks returning mocks; tests failing on refactoring even though behavior is unchanged. All of these signal a need for redesign, not more mocks.
Mockery::close()in tearDown() isn’t optional — without it, Mockery expectations aren’t verified and failing tests slip through.