skip to content

With a PHPUnit 13 test stub, how do you make a method return a value, choose one by argument, compute one, or throw?

level: juniorimportance: must knowfreq 56%

answer

  1. method() first, then an answer
  2. willReturn accepts several values
  3. willReturnMap row: arguments, then return
  4. willReturnCallback receives the call's arguments
  5. willThrowException takes a Throwable

basics

~10 s

Name the method with method(), then attach an answer: willReturn() for fixed values, willReturnMap() to pick by arguments, willReturnCallback() to compute from them, and willThrowException() to throw.

solid answer

~30 s

You always start with `$stub->method('name')` and then attach one answer. `willReturn($value)` returns the same value every time, and `willReturn($a, $b)` returns them on successive calls. `willReturnMap()` takes rows of arguments followed by the return value and picks the first row whose arguments match; if none matches it returns `null`, while `willReturnStrictMap()` fails the test instead. `willReturnCallback()` passes the call's arguments to your callable and returns its result, and `willThrowException(new SomeException())` throws on each call. `willReturn()` checks its values against the declared return type and throws `IncompatibleReturnValueException` for a mismatch, so `willReturn('yes')` on a `bool` method fails at configuration time.

code

php · 16 lines
php
<?php
declare(strict_types=1);

$users = self::createStub(UserRepository::class);
$users->method('existsByEmail')->willReturnMap([
    ['[email protected]', false],
    ['[email protected]', true],
]);

$hasher = self::createStub(PasswordHasher::class);
$hasher->method('hash')
    ->willReturnCallback(fn (string $plain): string => 'hashed:' . $plain);

$mailer = self::createStub(Mailer::class);
$mailer->method('send')
    ->willThrowException(new TransportException('SMTP down'));

go deeper

for a junior

Recall the chain method() then willReturn(), and which method throws, which computes and which looks up by arguments.

for a middle

Explain map row layout, what an unmatched map returns, sequence exhaustion, and the return-type check at configuration.

for a senior

Spot tests that pass only because generated defaults hide a missing configuration, and prefer strict maps where a miss matters.

for a principal

Weigh helper factories for common stubs against inline configuration when many tests share the same collaborators.

## The shape of every configuration A stub from `createStub()` (or a mock from `createMock()`) is configured in two steps: choose a method, then choose an answer. ```php $stub->method('existsByEmail')->willReturn(false); ``` `method()` accepts the method name as a string. A name that does not exist on the doubled type, or that belongs to a final or static method, is rejected with `MethodCannotBeConfiguredException` at configuration time. The answer methods live on the object `method()` returns, the `InvocationStubber`. ## The answer methods | Method | What each call returns | Typical use in a sign-up test | |---|---|---| | `willReturn($v)` | always `$v` | the address is free: `false` | | `willReturn($a, $b, $c)` | `$a`, then `$b`, then `$c` | first check free, second taken | | `willReturnOnConsecutiveCalls($a, $b)` | same as above | the dedicated sequence method | | `willReturnMap($rows)` | the row whose arguments match | different users per address | | `willReturnCallback($fn)` | `$fn(...$arguments)` | a fake password hasher | | `willThrowException($e)` | throws `$e` | the mail transport is down | | `willReturnSelf()` | the double itself | fluent builders | | `willReturnArgument($i)` | the argument at index `$i` | echo-style methods | A few details matter in interviews: - **Sequences run out.** After the last configured value, the next call throws `NoMoreReturnValuesConfiguredException` ("Only 2 return values have been configured ..."). It does not repeat the last value. - **Map rows are positional.** Each row lists the arguments in parameter order and the return value last: `['[email protected]', true]`. Plain values compare with `===`, and a PHPUnit constraint such as `$this->stringContains('@')` can stand in for an argument. - **An unmatched map returns `null`.** If the method is declared `: bool`, the generated method then fails with a `TypeError` for returning `null`, which is a confusing message. `willReturnStrictMap()` instead fails with "No entry in the value map matched the invocation of ...". - **A callback gets the real arguments.** `willReturnCallback(fn (string $plain): string => 'hashed:' . $plain)` receives exactly what the code under test passed. - **Throwing needs an object.** `willThrowException()` takes any `Throwable` instance, not a class name. ## Type checking at configuration time When `willReturn()` is called, PHPUnit compares each value with the method's declared return type. A value that does not fit throws `IncompatibleReturnValueException` with the message "Method existsByEmail may not return value of type string, its declared return type is "bool"". This check runs when you configure, so the mistake points at the test line rather than somewhere inside the service. Values produced by a map or a callback are not checked at configuration; a wrong type there surfaces later as a `TypeError` from the doubled method's return declaration. ## Methods you never configure Methods without an answer still work. PHPUnit generates a value from the declared return type: 1. `null` for nullable, `mixed` or `void` returns; 2. `false` for `bool`, `0` for `int`, `0.0` for `float`, `''` for `string`, `[]` for `array`; 3. a new test stub for an interface or class return type, so chained calls do not crash. For a union or intersection it cannot resolve, it throws an exception asking you to configure a return value. This generation is convenient but can hide a missing configuration: a repository stub whose `existsByEmail()` you forgot still returns `false`, and the "address already taken" branch is never exercised. ## Worked scenario In a `SignUpService` test, the repository stub decides which branch runs, the hasher stub computes a predictable hash, and a mailer stub can simulate an outage: - `existsByEmail` via `willReturnMap()` so two addresses behave differently; - `hash` via `willReturnCallback()` so the stored value is predictable; - `send` via `willThrowException(new TransportException('down'))` to prove the service reports a failed mail without losing the account. None of these checks how often a method was called. That is what `expects()` on a mock is for, and it belongs to a different question. ## Choosing between the answers When a stub needs more than one fixed value, work down this list and stop at the first fit: 1. **One value for every call**: `willReturn($value)`. Simple, and the intent is obvious to a reader. 2. **A fixed sequence of calls**: `willReturn($a, $b)`, when the order of calls is part of the scenario, such as a retry that succeeds on the second attempt. 3. **A value per argument**: `willReturnMap()`, or `willReturnStrictMap()` when a call nobody anticipated should fail loudly instead of returning `null`. 4. **A value computed from the arguments**: `willReturnCallback()`, when a table would be long or the result is derived, such as a predictable hash. 5. **A failure**: `willThrowException()`, to drive the error branch of the code under test. Picking the simplest answer keeps a test readable: a callback that only returns a constant hides a `willReturn()` behind extra code.

  • What happens when a stub configured with willReturn(true, false) is called a third time?
    The sequence is exhausted, so PHPUnit throws `NoMoreReturnValuesConfiguredException` saying only 2 return values have been configured. It does not fall back to the last value or to a generated default.
  • Why does a willReturnMap() stub sometimes produce a TypeError instead of a clear message?
    When no row matches, `willReturnMap()` returns `null`. If the doubled method declares a non-nullable return type, the generated method throws a `TypeError` for that `null`. Using `willReturnStrictMap()` makes the miss fail with a message naming the unmatched arguments.
  • What does PHPUnit return from a stub method you never configured?
    A value generated from the declared return type: `false`, `0`, `''`, `[]`, `null` for nullable types, or a new stub for an interface type. A union it cannot resolve makes PHPUnit throw and ask you to configure a value.

saying these in an interview costs you the question

  • willReturn repeats its last value once the sequence runs out
  • willReturnMap rows put the return value first
  • willThrowException takes the exception class name as a string
  • An unmatched willReturnMap call fails with a clear message by default
  • PHPUnit accepts any return value, whatever the declared return type