Mocking
phpspec 9 has a built-in mock system — no Prophecy required. Create test doubles, stub return values, and verify method calls with a clean, fluent API.
Creating mocks
mock() creates a test double that implements the given interface or extends the class:
$logger = mock(App\Logger::class); expect($logger)->toBeAnInstanceOf(App\Logger::class);
Stubbing return values
allow()->toReturn()
Stub a method to return a fixed value:
$repo = mock(App\UserRepository::class); allow($repo->find(1))->toReturn('Alice'); expect($repo->find(1))->toBe('Alice');
allow()->toReturnUsing()
Stub with a callback for dynamic returns:
allow($repo->find(1))->toReturnUsing( fn($id) => $id === 1 ? 'Alice' : null );
Argument-based dispatch
Different arguments return different values:
allow($repo->find(1))->toReturn('Alice'); allow($repo->find(2))->toReturn('Bob'); expect($repo->find(1))->toBe('Alice'); expect($repo->find(2))->toBe('Bob');
Catch-all stubs
Call allow() without arguments for a catch-all, then add specific overrides:
allow($repo->find())->toReturn('default'); allow($repo->find(42))->toReturn('special'); expect($repo->find(42))->toBe('special'); expect($repo->find(99))->toBe('default');
Verifying method calls
toBeCalled()
$logger = mock(App\Logger::class); expect($logger->log('hello'))->toBeCalled(); $logger->log('hello');
toBeCalledWith()
expect($logger->log('hello'))->toBeCalledWith('hello'); $logger->log('hello');
toBeCalledTimes()
$counter = mock(App\Counter::class); expect($counter->increment())->toBeCalledTimes(3); $counter->increment(); $counter->increment(); $counter->increment();
Fluent call count
// Exactly once expect($counter->increment())->toBeCalled()->once(); // Exactly N times expect($counter->increment())->toBeCalled()->exactly(3)->times(); // Never called expect($logger->log('anything'))->not()->toBeCalled();
Argument matchers
| Matcher | Description |
|---|---|
any() | Matches any single argument |
type('string') | Matches argument of given type |
callback(fn) | Matches if callback returns true |
anInstanceOf(Class) | Matches instanceof |
cetera() | Matches all remaining arguments |
noArgs() | Matches when called with no arguments |
// any() — matches any value expect($logger->log('msg'))->toBeCalledWith(any()); // anInstanceOf() — matches class expect($bus->dispatch(new Event('test'))) ->toBeCalledWith(anInstanceOf(Event::class)); // cetera() — matches trailing args expect($logger->log('info', 'msg')) ->toBeCalledWith('info', cetera());
Argument matchers also work in stub setup:
allow($repo->find(any()))->toReturn('fallback'); allow($repo->find(42))->toReturn('special');
Type-hinted mock injection
Type-hint parameters in it() or let() closures to auto-inject mocks:
// Mock injected via it() it('injects mocks by type hint', function (Mailer $mailer) { expect($mailer)->toBeAnInstanceOf(Mailer::class); }); // Mock injected via let() let('counter', fn(App\Counter $counter) => $counter);
Any parameter with an interface or class type hint is automatically resolved via mock().
Fresh mocks per example
let() values are re-evaluated for every example, so each example gets a fresh mock with no recorded calls:
describe('let reset', function() { let('counter', fn(App\Counter $counter) => $counter); it('first example calls increment', function() { expect($this->counter->increment())->toBeCalled()->once(); $this->counter->increment(); }); it('second example gets fresh mock', function() { expect($this->counter->increment())->not()->toBeCalled(); }); });
London School example
Mock collaborators and test in isolation:
use App\UserService; use App\UserRepository; describe(UserService::class, function () { it('returns the display name', function (UserRepository $repo) { allow($repo->find(1))->toReturn(['name' => 'Alice']); $service = new UserService($repo); expect($service->getDisplayName(1))->toBe('Alice'); }); it('returns Unknown for missing user', function (UserRepository $repo) { allow($repo->find(999))->toReturn(null); $service = new UserService($repo); expect($service->getDisplayName(999))->toBe('Unknown'); }); });