Expectations
This is the part you'll write the most: telling a double what you expect to happen, and what should happen when it does.
$repository->expects('find')->with(123)->returns($book);Read left to right, that's the whole sentence: expect a call to find, with 123, and return $book. Every modifier here has a sensible default if you leave it off.
expects() and allows()
Both register a configured call. The difference is what happens if it never happens:
$repository->expects('save'); // must be called exactly once; verify() fails otherwise$repository->allows('save'); // may be called any number of times, including zeroYou may reach for expects() when the call is the point of the test (the thing you're actually asserting happened), and allows() for setup that just needs to respond to a call, without the count being part of what you're testing.
Constraining arguments with with()
$repository->allows('find')->with(123)->returns($book);Leave with() off and the expectation matches a call to that method with any arguments. A plain value passed to with() is compared directly against the actual argument. See argument matching for exactly how that comparison works, and for matchers to reach for when an exact value is too strict.
Deciding what happens
$repository->allows('find')->with(123)->returns($book);$repository->allows('find')->with(999)->throws(new NotFoundException());$repository->allows('calculateTax')->resolves(fn (...$args) => $realGateway->calculateTax(...$args));returns(...)hands back a value.throws(...)throws an exception instead.resolves(...)computes the return value with a closure, given the real call's arguments. This is also how you may delegate a single call to a real object without switching the whole double to passthru.
If you leave all three off, a matched call falls back to the same safe default that Loose mode uses for an unmatched one, so an expectation without an explicit return doesn't hand back a bare null that turns into a TypeError further down.
Sequential returns
You may pass more than one value to returns() (or throws()), and each call receives the next value in the list, holding at the last one once the list runs out:
$repository->allows('find')->with(1)->returns($first, $second); $repository->find(1); // $first$repository->find(1); // $second$repository->find(1); // $second againThis is one expectation with a queue attached, not several competing expectations, so it composes cleanly with everything else on this page.
Note: Registering the same call twice to get a different answer each time — instead of one
returns()with several values — is ambiguous, since matching order (see below) means the most-recently-registered one wins first, handing back values in the reverse of what was written.verify()rejects this shape and points at the fix.
Counting calls with times()
$repository->expects('save')->times(3); // exactly 3$repository->expects('save')->times(1, 3); // between 1 and 3$repository->expects('save')->times(minimum: 2); // at least 2$repository->allows('save')->times(maximum: 5); // at most 5$repository->allows('save')->never(); // shorthand for times(0)One overloaded verb covers every count you'd want, rather than a separate word for each shape. never() remains as its own method because it reads more naturally than the equivalent times() call for that common case.
Matching order
When more than one expectation could match a call, the more specific one wins, regardless of which was declared first. An expectation is specific if its with() pins down at least one argument to something narrower than "anything" (a bare with(Argument::any()) doesn't count — it's the same as no with() at all). Between two equally specific expectations for the same call, the more recently declared one wins.
$repository->allows('find')->returns(null); // a default$repository->allows('find')->with(123)->returns($book); // a specific overridefind(123) gets $book and find(456) gets null no matter which of these two lines you write first. Still, write a broad default before its specific overrides — it reads the way it behaves, and it's the convention the rest of this library's examples follow.
Once a specific expectation's own times() budget is spent, matching falls back to the generic ones instead of throwing for a call it was never meant to serve.
Two expectations don't get ranked against each other for how narrow they are — with(123) and with(Argument::type('int')) are both just "specific," and between the two of them, ordinary declaration order applies.
Keeping calls in order
Most tests don't need to care what order unrelated calls happen in. But sometimes order is genuinely part of the contract: you can't commit() before beginTransaction(). For that, mark the relevant expectations ordered():
$connection->expects('open')->ordered();$connection->expects('write')->ordered();$connection->expects('close')->ordered();Calling write() before open() throws immediately, naming both methods involved. Expectations without ordered() are unaffected, and ordering is only checked within a single double.
Static methods
expects(), allows(), and received() only work with instance methods. There's no instance for a double to intercept a static call through. Configuring one is rejected up front, with a clear reason, rather than silently doing nothing:
$repository->expects('findAll'); // findAll() is `public static function`// Can't configure `findAll` on a double for `BookRepository` since// it's a static method. Static methods can't be doubled.Magic methods
Most magic methods can't be doubled, and configuring one is rejected up front:
$logger->expects('__get'); // __get() is a magic method// Can't configure `__get` on a double for `Logger` since// it's a magic method. Magic methods can't be doubled.The exceptions are __invoke, __toString, __serialize, __unserialize, and __clone — each has a fixed, ordinary method signature rather than PHP's dynamic-dispatch behavior, so a double configures and intercepts them exactly like any other method:
$verify = Double::for(VerifyPasskey::class); // an invokable single-action class$verify->expects('__invoke')->returns($passkey); $verify($credential, $options); // runs the double, not the real actionEverything else — __get, __set, __isset, __unset, __call, and __callStatic — stays rejected. Those exist to intercept access to members that don't exist at all, so there's no fixed call shape for expects()/allows() to match against.
__clone being configurable is separate from what a plain clone $double does on its own, with nothing configured: the clone is a fully working double, sharing the original's expectations and call history rather than starting blank — the same way cloning a Mockery mock carries its state over, since Mockery's own state lives in ordinary instance properties that PHP's default clone already copies. This matters most when it happens somewhere you didn't write it: real code you're exercising via passthru() may clone $this internally (Eloquent's relation builders do, for instance), and the clone it produces keeps working exactly like the double it came from.
This comes up most often with classes whose entire public API is __call-forwarded — AWS SDK clients, Redis connection wrappers, and similar. See why doesn't Double mock magic methods for what to double instead in those cases, and why it usually ends up being the stronger test.