sebastianbergmann/phpunit · error · ExpectationFailedException
Expected invocation at least once but it never occurred.
Error message
Expected invocation at least once but it never occurred.
What it means
PHPUnit throws this when a method configured with expects($this->atLeastOnce()) was never called at all during the test. InvokedAtLeastOnce::verify() runs when the mock is verified after the test body: it reads the recorded invocation count and fails because the count is zero. In short: your test promised the mocked method would be hit at least one time, and the code under test never reached that call.
Source
Thrown at src/Framework/MockObject/Runtime/Rule/InvokedAtLeastOnce.php:38
final class InvokedAtLeastOnce extends InvocationOrder
{
public function toString(): string
{
return 'invoked at least once';
}
/**
* Verifies that the current expectation is valid. If everything is OK the
* code should just return, if not it must throw an exception.
*
* @throws ExpectationFailedException
*/
public function verify(): void
{
$count = $this->numberOfInvocations();
if ($count < 1) {
throw new ExpectationFailedException(
'Expected invocation at least once but it never occurred.',
);
}
}
public function matches(BaseInvocation $invocation): bool
{
return true;
}
}
View on GitHub (pinned to f123cdb2a2)
Solutions
- Debug or trace the code under test to confirm which branch runs — usually an early return, failed precondition, or exception prevents the call.
- Verify the mock is actually injected into the object under test (constructor/setter argument), not created and left unused.
- Check the method name string passed to method() matches the collaborator's real method exactly.
- If the call legitimately does not happen on this path, remove the atLeastOnce() expectation or move it to a test that exercises the path where the call occurs.
- Prefer asserting the end state (returned value, persisted data) over strict interaction expectations when the interaction is incidental.
Example fix
// before
$repo->expects($this->atLeastOnce())->method('save');
$service->process($invalidDto); // validation fails, save() never called
// after: exercise the path that actually saves
$repo->expects($this->atLeastOnce())->method('save');
$service->process($validDto); Defensive patterns
Strategy: validation
Validate before calling
// Cheap pre-check: make sure the SUT actually reaches the interaction by asserting // a state change or return value the call produces before relying on atLeastOnce(). $this->assertNotNull($service->process($validDto)); // path that must call the collaborator
Prevention
- Inject the mock into the SUT in the same statement where you create it to avoid wiring mistakes.
- Double-check method() strings against the collaborator's interface; mocks of interfaces fail fast on typos only when called.
- Use typed test doubles (createMock(Interface::class)) so a wrong method name surfaces as a config error, not a silent pass.
- Cover the no-call branch with its own test so expectations are not blurred across paths.
When it happens
Trigger: $mock->expects($this->atLeastOnce())->method('save') followed by running code that never reaches save() — e.g. input validation fails first, the method name is misspelled in method('svae'), or the mocked dependency is not the instance the code under test actually uses.
Common situations: Guard clauses or exceptions in the SUT short-circuiting before the expected call; wiring a fresh mock via createMock() but injecting a different (real) object into the constructor; typos in method() names; tests that exercise an error path but still assert the happy-path interaction; refactoring that renamed the called method on the collaborator.
Related errors
- Expected invocation at least %d time%s but it occurred %d ti
- Expected invocation at most %d time%s but it occurred %d tim
- Method was expected to be called %d time%s, actually called
- Too many parameter sets given, %d out of %d expected paramet
- Doubled method does not exist.
AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23).
Data as JSON: /api/errors/9725be22b4619a6c.
Report an issue: GitHub.