Skip to content

Modes

A mode decides what an unmatched call does — a call nothing configured accepted.

ModeUnmatched call answers with
Loose (default)a type-safe default: null, 0, '', [], an empty generator …
Strict — Understudy::strict($double)an immediate failure naming the method, the call, and what did not accept it
Forwarding — Understudy::forwarding($double, $real)whatever the real instance answers, recorded like any other call

Strictness is per double, not per test. It is a different setting from strict stubs, which is about registrations nobody used.

What loose will and will not invent

A loose double never invents a value by running another class's constructor, and never hands back an unconstructed instance of a real class.

What it can hand back is another understudy: a return type that can itself be doubled becomes one, one level deep, which the same test can configure. That double is a generated stand-in, not the target with its constructor skipped.

One level, and no further. A double created this way refuses to produce another, so $a->b()->c() says so rather than inventing a third collaborator the test never asked for. Registering a factory for C is how you say you meant it — see Defaults registry.

Where no safe value exists, the double says so, and names the way out.

Strict fails at the call

text
Understudy `BookRepository` is strict and received an unexpected call to `tag()`.

The call was:
    tag('beta', 1)

Nothing configured for `tag` accepted it:
    tag(*string(matches: /^a/)*, *2*)

Configure it first: when(fn () => $double->tag(...))->returns(...)

The marks are read from the expectation's side: each one is an argument that rejected this call. Everything configured for that method is listed, because a stub that could never have matched is often exactly the one the test meant to write. See Failure messages.

Forwarding

Covered in full on Forwarding to a real object — it is a mode, but it carries enough of its own rules to need a page.