The call is the specification
when(fn () => $repo->find(1))->returns($book) — the method is called, so a rename, a wrong argument type or a dropped parameter is a type error before the test runs.
No method names in strings. One engine, two runner adapters and two static analysers.
understudy — the engine: doubles, stubbing, expectations, verification, transcripts. Depends on nothing but PHP.understudy-testo — verification and reset wired into Testo's lifecycle, with Fiber isolation.understudy-phpunit — the same, through a trait; works under Pest.understudy-psalm — a Psalm plugin that types the specification closure and reports misuse.understudy-phpstan — the same checks as PHPStan rules, under the understudy.* identifiers.Install the engine plus exactly the adapter your suite already uses. The static analysers are independent of both: either one can be added on its own.
An object argument is matched by identity, so two instances never match however equally they read. The message has to be able to say which of the two reasons a call failed — and it does:
Understudy `BookRepository` expected `save(App\Book#1 {title: 'Dune'})` to be called
exactly 2 times, but it was called 1 time.
The following calls to `save` were made during this test:
save(App\Book#1 {title: 'Dune'})
save(*App\Book#2 {title: 'Dune'}*)#1 and #2 are aliases numbered within one message, in order of first appearance — not object ids, which are reused after a collection and would print differently on the next run. The asterisks mark what differed.
See the Cookbook for this and four other real incidents, each with a script you can run.