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

  1. Compare the actual count in the message with your upper bound and inspect why the extra calls happen (loops, retries, event dispatch).
  2. If the extra calls are legitimate, raise the bound (atMost(N)) or use exactly(N) when the count is deterministic.
  3. 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.
  4. 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

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


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