Mocking

PhpSpec includes a built-in mocking system, no external libraries required. It generates test doubles at runtime using reflection and eval().

Creating Doubles

Using mock()

Use the global mock() function:

$double = mock(UserRepository::class);

This generates a dynamic subclass (or implementation, for interfaces) that:

  • Extends the original class (or implements the interface)
  • Overrides all methods to track calls
  • Auto-generates constructor dependencies recursively (using mocks/defaults)

Using dummy()

A double from mock() is strict: a call to it that the spec never declared, made from the code under spec, fails the example unless a toHaveBeenCalled() after the act checks it. dummy() makes a lenient double for a collaborator whose calls do not matter, a logger passed along for instance. It answers every call with a default for the return type and still takes stubs:

$logger = dummy(Logger::class);

Type-Hinted Mock Injection

Both it() and this() closures support type-hinted parameters that are automatically resolved as mocks:

describe(UserService::class, function () {
    // Injected via this()
    this('service', fn (UserRepository $repo) => new UserService($repo));

    // Injected via it()
    it('finds a user', function (UserRepository $repo) {
        allow($repo->find(1))->toReturn(['name' => 'Alice']);
        $service = new UserService($repo);
        expect($service->getDisplayName(1))->toBe('Alice');
    });
});

Any parameter with a class or interface type hint will be automatically resolved via mock(). This is the preferred way to create mocks.

Stubbing Return Values

allow() + toReturn()

The allow() function stubs a method to return a specific value:

it('returns user data', function (UserRepository $repo) {
    allow($repo->find(1))->toReturn(['name' => 'Alice']);

    $result = $repo->find(1);
    expect($result)->toBe(['name' => 'Alice']);
});

andReturn() is another name for toReturn().

The value has to be one the real method could return, held strictly: a method returning int is not given '42', and one returning void is not given anything, its call is declared with allow() alone. A value it could never return is refused where the spec sets it:

App\Counter::count() returns int, so it cannot return string "three".

What a toReturnUsing() callback answers is held the same way when the call is made.

allow() + toThrow()

Stub a method to throw an exception:

it('handles errors', function (UserRepository $repo) {
    allow($repo->find(999))->toThrow(new \RuntimeException('Not found'));

    expect(fn () => $repo->find(999))->toThrow(\RuntimeException::class);
});

Give it the very instance to throw, as above, or a class name and a message and it builds one at the call: toThrow(\RuntimeException::class, 'Not found').

Stubs answer in the order they were declared

Each call the code under spec makes is answered by the first stub that matches it and has not answered yet; once every matching stub has answered, the last one answers on. Two stubs of the same call answer one call after the other:

allow($repo->find(1))->toThrow(new \RuntimeException('Busy'));
allow($repo->find(1))->toReturn('Alice'); // the second find(1), and every one after

Stubs that overlap are used in order too: a find(42) declared after a find(any()) answers only once find(any()) has. A stub every example needs belongs in beforeEach; one an example would replace belongs in that example.

The spec's own calls use no stub up and have its exception withheld, so the second allow($repo->find(1)) above declares the call rather than throwing. They see the value the next stub would give, and toThrow() sees the exception it would throw: expect(fn () => $repo->find(1))->toThrow(...).

What an unstubbed call returns

A method nobody stubbed returns a default for its return type: '', 0, false, [], null where allowed, a double of a class or interface it returns, an empty iterable, a Closure, an empty Generator, true or false for those types, the double itself for static, self and parent. A method returning a final class hands the spec an instance of it built without its constructor, for allow() to replace; a dummy() has nobody about to replace it and says so instead.

Calls the spec never declared

A double is strict towards the code under spec. A call that no allow() or expect() declared and no toHaveBeenCalled() checks, made from anywhere outside the spec code, fails the example. The spec code is every file PhpSpec loaded as a spec, step or support file, wherever it sits, and whatever sits in the spec, features and suite folders, or the folder of a path the run was given:

App\Stock::reserve("tea", 2) was called but not expected, at src/App/Checkout.php:8.

A call is declared by the arguments it was written with, read as PHP reads the call: any() stands for one argument whose value does not matter, cetera() for the rest, and a call that leaves out an argument with a default is the call with that default. allow() declares on its own, a stub is optional:

allow($stock->reserve('tea', 2));                 // reserve('tea', 2) may be called, returns false
allow($stock->reserve('tea', 2))->toReturn(true); // and returns true
expect($stock->reserve(cetera()))->toBeCalled();  // any reserve() may be called, and must be

reserve() is a call with no arguments, which no object taking two could be given, so it is refused where the spec writes it:

App\Stock::reserve() takes 2 arguments (string $item, int $quantity), so a call written with none can never be made.
Write the arguments it is called with, any() for one whose value does not matter, or cetera() for the rest.

An argument of a type the method does not take is refused the same way. The code under spec calling a double with too few arguments, or with an argument of the wrong type, fails as it would on the real object, its argument coerced or held strictly as the file it calls from declares:

App\Stock::reserve(): Argument #2 ($quantity) must be of type int, string given, called in src/App/Checkout.php on line 11

The spec file itself is free to call anything: that is how allow() and expect() reach the call they declare. A collaborator whose calls do not matter is a dummy(). A double that was injected by type hint, which dummy() cannot make, is left lenient for the rest by ending a stub in ignoreOthers():

beforeEach(function (Filesystem $fs) {
    allow($fs->exists(any()))->toReturn(false);
    allow($fs->isDir(any()))->toReturn(false)->ignoreOthers();
});

Verifying Method Calls

Stub and verify the same call in any order: a stubbed call still records that it was made, and its returned value still compares with the ordinary matchers.

toBeCalled()

Declared before the act, judged when the example ends: the method must be called at least once by then. Arguments written in the expect call are part of the promise: save($user) is not satisfied by a save() of something else, save(any()) by a save() of anything. The declaration also makes the call expected, so the code under spec may make it:

it('calls save', function (UserRepository $repo) {
    expect($repo->save($user))->toBeCalled();

    (new Registration($repo))->register($user);
});

it('saves something', function (UserRepository $repo) {
    expect($repo->save(any()))->toBeCalled();

    (new Registration($repo))->register($anyUser);
});

toHaveBeenCalled()

Asked after the act, judged at once from the calls made so far. Nothing needs declaring before the act.

it('saves the user', function (UserRepository $repo) {
    (new Registration($repo))->register($user);

    expect($repo->save($user))->toHaveBeenCalled();
});

Negated, it asks that the call was not made: expect($repo->save($other))->not()->toHaveBeenCalled().

A count follows it, judged from the same calls: ->once(), ->twice(), ->exactly(3)->times(), ->atLeast(2)->times() or ->atMost(2)->times().

expect($mailer->send(any()))->toHaveBeenCalled()->twice();

Counting calls

toBeCalled() takes how many times, counting the calls that match the arguments the call was written with; a call that must not happen is not()->toBeCalled():

expect($logger->log('test'))->toBeCalled()->once();
expect($logger->log('test'))->toBeCalled()->twice();
expect($logger->log('test'))->toBeCalled()->exactly(3)->times();
expect($logger->log('test'))->toBeCalled()->atLeast(2)->times();
expect($logger->log('test'))->toBeCalled()->atMost(2)->times();
expect($logger->log('other'))->not()->toBeCalled();

Argument Matchers

An argument written as a value matches by value: a scalar strictly, an object when it equals the one written (new Money(900) matches any Money(900), whichever instance carries it), an array element by element on the same rule. For anything looser, write a matcher in the call:

any()

Matches any single argument:

expect($logger->log(any()))->toBeCalled();

type(string $type)

Matches an argument of a specific type:

expect($repo->save(type('object')))->toBeCalled();

callback(Closure $fn)

Matches using a custom callback:

expect($repo->save(callback(fn ($arg) => $arg->name === 'Alice')))->toBeCalled();

satisfy(Closure $fn)

Matches a single argument against a predicate, as callback() does, and reads well alongside the others:

expect($repo->save(satisfy(fn ($u) => $u->isActive())))->toBeCalled();

anInstanceOf(string $class)

Matches an argument that is an instance of the given class or interface:

expect($bus->dispatch(anInstanceOf(Command::class)))->toBeCalled();

arrayIncluding(array $subset)

Matches an array argument that contains (at least) the given key/value subset:

expect($api->send(arrayIncluding(['type' => 'order'])))->toBeCalled();

startWith(string $prefix)

Matches a string argument that starts with the given prefix:

expect($logger->log(startWith('ERROR:')))->toBeCalled();

cetera()

Matches the remaining arguments, none or more: the "and so on" wildcard. Nothing can follow it:

expect($logger->log('hello', cetera()))->toBeCalled();

Negation

All mock matchers support negation via not():

expect($repo->delete(1))->not()->toBeCalled();
expect($logger->log('other'))->not()->toBeCalled();

Using Mocks with this()

describe(Notifier::class, function () {
    this('mailer', fn () => mock(Mailer::class));
    this('notifier', fn () => new Notifier($this->mailer));

    it('sends an email', function () {
        expect($this->mailer->send('hello'))->toBeCalled();
        $this->notifier->notify('hello');
    });
});

Or using type-hinted injection in this():

describe(Notifier::class, function () {
    this('notifier', fn (Mailer $mailer) => new Notifier($mailer));

    it('sends an email', function () {
        // $this->mailer is available from this() injection
    });
});

Coming from Prophecy

A call is written as in Prophecy, as the call itself, and read the same way: $repo->find() is a call with no arguments, never find(1). Where the method requires an argument, that call is refused where the spec writes it, so a not()->toBeCalled() can never pass for a call that could not happen.

ProphecyPhpSpec 9
$repo->find(1)->willReturn('Alice')allow($repo->find(1))->toReturn('Alice')
$repo->find(1)->willThrow(NotFound::class)allow($repo->find(1))->toThrow(NotFound::class)
$repo->find(1)->will(fn ($args) => ...)allow($repo->find(1))->toReturnUsing(fn ($id) => ...)
$repo->save($user)->shouldBeCalled()expect($repo->save($user))->toBeCalled()
$repo->save($user)->shouldBeCalledTimes(2)expect($repo->save($user))->toBeCalled()->exactly(2)->times()
$repo->save(Argument::cetera())->shouldNotBeCalled()expect($repo->save(cetera()))->not()->toBeCalled()
$repo->save($user)->shouldHaveBeenCalled()expect($repo->save($user))->toHaveBeenCalled()
$repo->save($user)->shouldHaveBeenCalledTimes(2)expect($repo->save($user))->toHaveBeenCalled()->twice()
Argument::any(), Argument::cetera()any(), cetera()
Argument::type('string')type('string')
Argument::that(fn ($user) => ...)satisfy(fn ($user) => ...)

Supported Types

The mock system handles:

  • Classes: generates a subclass
  • Interfaces: generates an implementation
  • Nullable types (?Foo): wraps return with null handling
  • Union types (Foo|Bar): uses the first mockable type
  • Intersection types (Foo&Bar): creates a combined implementation
  • Enum types: returns the first case as default
  • Scalar return types (string, int, float, bool, array): returns type-appropriate defaults

Limitations

  • Final classes cannot be mocked (PHP reflection limitation)
  • Readonly classes cannot be mocked
  • Constructor dependency auto-resolution handles classes, interfaces, strings, arrays, and scalars
  • Circular type dependencies are capped at depth 3