Skip to content

Strict stubs

A stub is permission, so an unused one does not fail a test by default. Strict stubs reverse that:

php
Understudy::verifyAll(strictStubs: true);

A stub configured but never called then fails its test too — the Mockito reading of "why did you configure it, then?".

Why it is off by default

Both readings are defensible, and understudy takes the position that stubbing describes a collaborator while expecting asserts on it. A test that stubs three methods of a repository and exercises a path through two of them is describing the repository, and failing it teaches nothing.

The reading flips once a suite grows: an unused stub is then more often a stub that stopped being used — the code moved, and the test kept a description of a call that no longer happens. That is a real signal, and it is why this is worth turning on project-wide rather than per test.

Turning it on

php
new UnderstudyPlugin(strictStubs: true)
php
abstract class ProjectTestCase extends TestCase
{
    use UnderstudyPHPUnitIntegration;

    protected function understudyStrictStubs(): bool
    {
        return true;
    }
}
php
Understudy::verifyAll(strictStubs: true);

Strictness per double is a different thing

Do not confuse the two:

ScopeFails on
verifyAll(strictStubs: true)the whole verificationa stub that was never used
Understudy::strict($double)one doublea call nothing configured accepted

The first is about registrations that went unused. The second is a mode, about calls that arrived unconfigured — and it fails at the call rather than at verification. They are independent: per-double strictness is available from the core regardless of the adapter setting.

The failure

See the cookbook case for the message and for why the unused stub was usually the bug.