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

  1. 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.
  2. If an exception legitimately aborts the flow, use expectException() and change the expectation to ->never() or remove it.
  3. Verify you are asserting on the same mock instance the subject actually uses (no new mock created inside the subject).
  4. 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

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


AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23). Data as JSON: /api/errors/b936f8378f5e7148. Report an issue: GitHub.