sebastianbergmann/phpunit · error · ExpectationFailedException
%s was not expected to be called more than %d times, actuall
Error message
%s was not expected to be called more than %d times, actually called %d time%s.
What it means
Unlike the verification-time variant, this PHPUnit error is thrown during the test itself, at the moment the (N+1)-th call hits the mock. InvokedCount::invokedDo() counts invocations as they happen and throws as soon as the count exceeds the configured exactly(N) / once() / never() limit. The message includes the concrete invocation (class::method with arguments) that went over budget, which pinpoints the offending call in your stack trace.
Source
Thrown at src/Framework/MockObject/Runtime/Rule/InvokedCount.php:96
protected function invokedDo(BaseInvocation $invocation): void
{
$count = $this->numberOfInvocations();
if ($count > $this->expectedCount) {
$message = $invocation->toString() . ' ';
$message .= match ($this->expectedCount) {
0 => 'was not expected to be called',
1 => 'was not expected to be called more than once',
default => sprintf(
'was not expected to be called more than %d times',
$this->expectedCount,
),
};
$message .= sprintf(', actually called %d time%s.', $count, $count !== 1 ? 's' : '');
throw new ExpectationFailedException($message);
}
}
}
View on GitHub (pinned to f123cdb2a2)
Solutions
- Use the included invocation string and stack trace to identify exactly which call exceeded the limit.
- If the extra call is legitimate, raise the expectation (exactly(N+1)) or relax it to atMost(N) / any().
- For never() failures, hunt the hidden call site — enable a xdebug backtrace or add a die()/log inside a willReturnCallback to see who calls it.
- If the excess call reveals duplicate work in production code (double submit, missing idempotency guard), fix that code path.
- If the count depends on data, make the fixture deterministic or assert against count($fixture) instead of a hardcoded number.
Example fix
// before
$db->expects($this->never())->method('rollback');
$service->import($rows); // a failing row silently triggers rollback()
// after: assert the rollback explicitly (or fix the import so it does not roll back)
$db->expects($this->once())->method('rollback'); Defensive patterns
Strategy: try-catch
Try / catch
// When diagnosing which call site breaks a never()/once() limit, let the exception
// carry the trace out of the SUT and assert on it deliberately:
try {
$service->run();
} catch (\PHPUnit\Framework\ExpectationFailedException $e) {
self::fail('Invocation limit exceeded: ' . $e->getMessage());
} Prevention
- never() is best combined with a backtrace capture: ->willReturnCallback(fn () => throw new LogicException('must not be called')) gives a clearer failure than the count message.
- Keep loop-driven calls matched by deriving counts from the fixture.
- Avoid strict once() expectations on methods that caching or short-circuits may skip or repeat.
- Inspect the invocation string embedded in the message — it names the exact arguments of the offending call.
When it happens
Trigger: $mock->expects($this->once())->method('connect') where the code under test calls connect() a second time; never() (expectedCount 0) where the method is called at all — the very first invocation throws 'was not expected to be called'; exactly(N) exceeded mid-loop. Because it throws at call time, the test aborts at the excess invocation rather than at mock verification.
Common situations: never() stubs hit by a hidden call such as a destructor, flush(), logger, or error handler; once() expectations broken when a fix adds a retry or a second iteration; recursive algorithms invoking the collaborator once per level; mocks created with createMock() whose methods return null and cause the SUT to retry; eager-loading or event systems dispatching more times than assumed.
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
- %s was expected to be %s but was %s.
- Expected invocation at least once but it never occurred.
AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23).
Data as JSON: /api/errors/26713b5e16e5c064.
Report an issue: GitHub.