{"record":{"id":"db5571bd1cdc4b21","repo":"sebastianbergmann/phpunit","slug":"expected-invocation-at-most-d-time-s-but-it-occur","errorCode":null,"errorMessage":"Expected invocation at most %d time%s but it occurred %d time%s.","messagePattern":"Expected invocation at most (.+?) time(.+?) but it occurred (.+?) time(.+?)\\.","errorType":"exception","errorClass":"ExpectationFailedException","httpStatus":null,"severity":"error","filePath":"src/Framework/MockObject/Runtime/Rule/InvokedAtMostCount.php","lineNumber":53,"sourceCode":"\n        return sprintf(\n            'invoked at most %d times',\n            $this->allowedInvocations,\n        );\n    }\n\n    /**\n     * Verifies that the current expectation is valid. If everything is OK the\n     * code should just return, if not it must throw an exception.\n     *\n     * @throws ExpectationFailedException\n     */\n    public function verify(): void\n    {\n        $actualInvocations = $this->numberOfInvocations();\n\n        if ($actualInvocations > $this->allowedInvocations) {\n            throw new ExpectationFailedException(\n                sprintf(\n                    'Expected invocation at most %d time%s but it occurred %d time%s.',\n                    $this->allowedInvocations,\n                    $this->allowedInvocations !== 1 ? 's' : '',\n                    $actualInvocations,\n                    $actualInvocations !== 1 ? 's' : '',\n                ),\n            );\n        }\n    }\n\n    public function matches(BaseInvocation $invocation): bool\n    {\n        return true;\n    }\n}\n","sourceCodeStart":35,"sourceCodeEnd":70,"githubUrl":"https://github.com/sebastianbergmann/phpunit/blob/f123cdb2a2d49f15025794166cfed8bda8627dd2/src/Framework/MockObject/Runtime/Rule/InvokedAtMostCount.php#L35-L70","documentation":"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'.","triggerScenarios":"$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.","commonSituations":"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.","solutions":["Compare the actual count in the message with your upper bound and inspect why the extra calls happen (loops, retries, event dispatch).","If the extra calls are legitimate, raise the bound (atMost(N)) or use exactly(N) when the count is deterministic.","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.","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."],"exampleFix":"// before\n$mailer->expects($this->atMost(1))->method('send');\nforeach ($orders as $order) { $notifier->notify($order); } // sends one mail per order\n\n// after: bound matches the actual contract\n$mailer->expects($this->atMost(count($orders)))->method('send');","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use atMost(N) only when the upper bound is part of the contract; otherwise use exactly(N) or atLeastOnce().","Derive bounds from the fixture (atMost(count($items))) instead of hardcoding numbers.","Watch for hidden extra calls: destructors, flush(), event listeners, retry loops.","Prefer never() for 'must not happen' and exactly() for 'must happen k times' — atMost leaves the lower end unchecked."],"tags":["phpunit","mock","verification","invocation-count"],"backgroundTag":"mock-invocation-count-mismatch","analyzedSha":"f123cdb2a2d49f15025794166cfed8bda8627dd2","analyzedAt":"2026-08-23T01:20:58.058Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}