sebastianbergmann/phpunit · error · ExpectationFailedException
Method was expected to be called %d time%s, actually called
Error message
Method was expected to be called %d time%s, actually called %d time%s.
What it means
PHPUnit throws this at mock-verification time when a method stubbed with expects($this->exactly(N)) (or once(), twice(), times(N), never()) was called a different number of times than expected. InvokedCount::verify() runs after the test body and requires the recorded count to equal the expected count exactly — both too few and too many calls fail here (if the test survived that long; see the invokedDo error for the mid-test variant).
Source
Thrown at src/Framework/MockObject/Runtime/Rule/InvokedCount.php:63
}
public function matches(BaseInvocation $invocation): bool
{
return true;
}
/**
* 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
{
$actualCount = $this->numberOfInvocations();
if ($actualCount !== $this->expectedCount) {
throw new ExpectationFailedException(
sprintf(
'Method was expected to be called %d time%s, actually called %d time%s.',
$this->expectedCount,
$this->expectedCount !== 1 ? 's' : '',
$actualCount,
$actualCount !== 1 ? 's' : '',
),
);
}
}
/**
* @throws ExpectationFailedException
*/
protected function invokedDo(BaseInvocation $invocation): void
{
$count = $this->numberOfInvocations();
View on GitHub (pinned to f123cdb2a2)
Solutions
- Read the expected vs actually-called counts in the message and decide which one reflects the real contract.
- If the actual count is correct, update the expectation: exactly(3) instead of exactly(2), or switch to atLeast()/atMost() when the exact count is not part of the contract.
- If the expected count is correct, debug why the code under test calls the method a different number of times (extra loop iteration, missing return, double dispatch).
- For zero-times assertions use never() — but note that even one invocation fails verification with this message context.
- When you only care that the method ran, prefer expects($this->atLeastOnce()) to make the test less brittle.
Example fix
// before
$cache->expects($this->exactly(2))->method('get');
$service->lookup($id); // first call warms the cache, so get() runs only once
// after: assert the count the code really produces, or loosen it
$cache->expects($this->atLeast(1))->method('get'); Defensive patterns
Strategy: validation
Prevention
- Use exactly(N) only with deterministic inputs; derive N from the fixture when possible.
- Prefer atLeastOnce() when the precise count is incidental to the behavior under test.
- Keep stubbed interactions minimal: each expects() you add is a contract the SUT must satisfy.
- Re-run the suite after changing loop bounds, caching, or conditional call paths.
When it happens
Trigger: $mock->expects($this->exactly(2))->method('log') while the code calls log() once or three times; once() combined with code that calls the method zero or two times; never() where the method ends up being called at least once.
Common situations: Conditional logic (caching, feature flags, short-circuit evaluation) changing how many times a collaborator is hit; loops whose iteration count depends on fixtures; refactors that hoist or duplicate a call; once() used as a lazy 'it gets called' assertion that breaks when a second legitimate call is added; concurrent/recursive code paths multiplying invocations.
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
- 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/b3d9ca8165aded32.
Report an issue: GitHub.