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

  1. Use the included invocation string and stack trace to identify exactly which call exceeded the limit.
  2. If the extra call is legitimate, raise the expectation (exactly(N+1)) or relax it to atMost(N) / any().
  3. 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.
  4. If the excess call reveals duplicate work in production code (double submit, missing idempotency guard), fix that code path.
  5. 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

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


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