sebastianbergmann/phpunit · error · ExpectationFailedException
Expected invocation at most %d time%s but it occurred %d tim
Error message
Expected invocation at most %d time%s but it occurred %d time%s.
What it means
PHPUnit throws this at mock-verification time when a method stubbed with expects($this->atMost(N)) was invoked more than N times. InvokedAtMostCount::verify() compares the recorded invocation count against the upper bound you configured and fails if the code under test exceeded it. It is an interaction-frequency assertion: 'this method must not be called more often than N times'.
Source
Thrown at src/Framework/MockObject/Runtime/Rule/InvokedAtMostCount.php:53
return sprintf(
'invoked at most %d times',
$this->allowedInvocations,
);
}
/**
* 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
{
$actualInvocations = $this->numberOfInvocations();
if ($actualInvocations > $this->allowedInvocations) {
throw new ExpectationFailedException(
sprintf(
'Expected invocation at most %d time%s but it occurred %d time%s.',
$this->allowedInvocations,
$this->allowedInvocations !== 1 ? 's' : '',
$actualInvocations,
$actualInvocations !== 1 ? 's' : '',
),
);
}
}
public function matches(BaseInvocation $invocation): bool
{
return true;
}
}
View on GitHub (pinned to f123cdb2a2)
Solutions
- Compare the actual count in the message with your upper bound and inspect why the extra calls happen (loops, retries, event dispatch).
- If the extra calls are legitimate, raise the bound (atMost(N)) or use exactly(N) when the count is deterministic.
- If the extra calls are a bug (duplicate dispatch, missing early return), fix the production code so it stops after the allowed number of calls.
- For unit tests where the count is noisy, replace atMost() with a recording spy and assert on the count explicitly for a clearer failure message.
Example fix
// before
$mailer->expects($this->atMost(1))->method('send');
foreach ($orders as $order) { $notifier->notify($order); } // sends one mail per order
// after: bound matches the actual contract
$mailer->expects($this->atMost(count($orders)))->method('send'); Defensive patterns
Strategy: validation
Prevention
- Use atMost(N) only when the upper bound is part of the contract; otherwise use exactly(N) or atLeastOnce().
- Derive bounds from the fixture (atMost(count($items))) instead of hardcoding numbers.
- Watch for hidden extra calls: destructors, flush(), event listeners, retry loops.
- Prefer never() for 'must not happen' and exactly() for 'must happen k times' — atMost leaves the lower end unchecked.
When it happens
Trigger: $mock->expects($this->atMost(1))->method('send') while the code under test calls send() two or more times — e.g. a loop that iterates more items than expected, a retry mechanism firing, or an event handler invoked once per item. The failure appears when PHPUnit verifies the mock after the test body, not at call time.
Common situations: Batch-processing code that processes more rows than the fixture contains; retry logic or event listeners adding extra calls; atMost(1) used where the code legitimately calls the method once per item; changing collection sizes in fixtures without updating the upper bound; expecting atMost(0)/never() while a destructor, flush, or shutdown hook still triggers the call.
Related errors
- Expected invocation at least %d time%s but it occurred %d ti
- Method was expected to be called %d time%s, actually called
- Expected invocation at least once but it never occurred.
- %s was not expected to be called more than %d times, actuall
- Too many parameter sets given, %d out of %d expected paramet
AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23).
Data as JSON: /api/errors/db5571bd1cdc4b21.
Report an issue: GitHub.