sebastianbergmann/phpunit · error · ExpectationFailedException
%s was expected to be %s but was %s.
Error message
%s was expected to be %s but was %s.
What it means
Classic PHPUnit verification failure from Matcher::verify(): at the end of the test the invocation-count rule (once/exactly(n)/atLeast(...)...) did not match reality, so it throws '<expectation> was expected to be <rule> but was <invoked N times/never invoked>'. Note the exemptions: AnyInvokedCount, never-expectations and atMost() rules skip this count message (parameters are verified separately), and the count text distinguishes 'never invoked', 'invoked once', 'invoked N times'.
Source
Thrown at src/Framework/MockObject/Runtime/Matcher.php:230
}
try {
$this->invocationRule->verify();
} catch (ExpectationFailedException) {
$actual = $this->invocationRule->numberOfInvocations();
if ($actual === 0) {
$invoked = 'never invoked';
} elseif ($actual === 1) {
$invoked = 'invoked once';
} else {
$invoked = sprintf(
'invoked %d times',
$actual,
);
}
throw new ExpectationFailedException(
sprintf(
'%s was expected to be %s but was %s.',
$this->methodNameRule->failureDescription($this->className),
$this->invocationRule->toString(),
$invoked,
),
);
}
if ($this->parametersRule === null) {
$this->parametersRule = new AnyParameters;
}
$invocationIsAny = $this->invocationRule instanceof AnyInvokedCount;
$invocationIsNever = $this->invocationRule instanceof InvokedCount && $this->invocationRule->isNever();
$invocationIsAtMost = $this->invocationRule instanceof InvokedAtMostCount;
if (!$invocationIsAny && !$invocationIsNever && !$invocationIsAtMost) {View on GitHub (pinned to f123cdb2a2)
Solutions
- Read the message: it states the rule (e.g. 'invoked once') versus the actual count — adjust the expectation (once -> any/never/exactly) or fix the subject so it behaves as the test asserts.
- If an exception legitimately aborts the flow, use expectException() and change the expectation to ->never() or remove it.
- Verify you are asserting on the same mock instance the subject actually uses (no new mock created inside the subject).
- For call-count changes from retries/loops, model them explicitly with exactly()/atLeast() based on the real contract.
Example fix
// before
$logger = $this->createMock(Logger::class);
$logger->expects($this->once())->method('persist');
$service->run($logger); // run() returns early, never persists
// after
$logger->expects($this->never())->method('persist');
$service->run($logger); Defensive patterns
Strategy: validation
Validate before calling
// make the contract explicit and cheap to check: list expected calls before acting $this->expectedCalls = ['save' => 1]; // after acting, cheap pre-verification is not possible generically — // instead keep expectations minimal and derived from the documented behavior
Type guard
// interrogate the double before verify(): cheap smoke check in helpers // (internal API) count invocations that already happened $handler = $mock->__phpunit_getInvocationHandler(); // use only for debugging test scaffolding, not production logic
Try / catch
try {
$mock->__phpunit_verify(); // or let the TestCase run verification
} catch (PHPUnit\Framework\ExpectationFailedException $e) {
// count mismatch: read 'was expected to be invoked once but was never invoked',
// adjust the rule (never/any/exactly) or fix the subject's control flow
} Prevention
- Derive expectations from documented behavior, not implementation details; count-based tests are brittle.
- When the subject may abort early, assert the exception first and relax call-count expectations.
- Double-check that the subject received the same mock instance you configured.
When it happens
Trigger: ->expects($this->once())->method('save') but the subject never calls save(); expects($this->exactly(2)) when the method ran 3 times; expects($this->atLeastOnce()) with zero calls; the call happened on a different mock than the one asserted; the expectation was created after the call happened on a sealed/mock object so it could not register.
Common situations: Caching/short-circuit logic in the subject skipping the collaborator call; exception thrown mid-flow before the collaborator is reached (assert the exception first or expect the call not to happen); tests using createMock() and asserting on a stale instance after the subject received a fresh one; refactors changing how many times a helper is invoked (loops, retries).
Related errors
- Expectation for %s failed. %s
- Cannot double method with invalid name "%s"
- Cannot double using a method list that contains duplicates:
- Cannot use "%s" as the name of a test double class because i
- The name "%s" is already in use
AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23).
Data as JSON: /api/errors/b936f8378f5e7148.
Report an issue: GitHub.