{"record":{"id":"9725be22b4619a6c","repo":"sebastianbergmann/phpunit","slug":"expected-invocation-at-least-once-but-it-never-occ","errorCode":null,"errorMessage":"Expected invocation at least once but it never occurred.","messagePattern":"Expected invocation at least once but it never occurred\\.","errorType":"exception","errorClass":"ExpectationFailedException","httpStatus":null,"severity":"error","filePath":"src/Framework/MockObject/Runtime/Rule/InvokedAtLeastOnce.php","lineNumber":38,"sourceCode":"final class InvokedAtLeastOnce extends InvocationOrder\n{\n    public function toString(): string\n    {\n        return 'invoked at least once';\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        $count = $this->numberOfInvocations();\n\n        if ($count < 1) {\n            throw new ExpectationFailedException(\n                'Expected invocation at least once but it never occurred.',\n            );\n        }\n    }\n\n    public function matches(BaseInvocation $invocation): bool\n    {\n        return true;\n    }\n}\n","sourceCodeStart":20,"sourceCodeEnd":49,"githubUrl":"https://github.com/sebastianbergmann/phpunit/blob/f123cdb2a2d49f15025794166cfed8bda8627dd2/src/Framework/MockObject/Runtime/Rule/InvokedAtLeastOnce.php#L20-L49","documentation":"PHPUnit throws this when a method configured with expects($this->atLeastOnce()) was never called at all during the test. InvokedAtLeastOnce::verify() runs when the mock is verified after the test body: it reads the recorded invocation count and fails because the count is zero. In short: your test promised the mocked method would be hit at least one time, and the code under test never reached that call.","triggerScenarios":"$mock->expects($this->atLeastOnce())->method('save') followed by running code that never reaches save() — e.g. input validation fails first, the method name is misspelled in method('svae'), or the mocked dependency is not the instance the code under test actually uses.","commonSituations":"Guard clauses or exceptions in the SUT short-circuiting before the expected call; wiring a fresh mock via createMock() but injecting a different (real) object into the constructor; typos in method() names; tests that exercise an error path but still assert the happy-path interaction; refactoring that renamed the called method on the collaborator.","solutions":["Debug or trace the code under test to confirm which branch runs — usually an early return, failed precondition, or exception prevents the call.","Verify the mock is actually injected into the object under test (constructor/setter argument), not created and left unused.","Check the method name string passed to method() matches the collaborator's real method exactly.","If the call legitimately does not happen on this path, remove the atLeastOnce() expectation or move it to a test that exercises the path where the call occurs.","Prefer asserting the end state (returned value, persisted data) over strict interaction expectations when the interaction is incidental."],"exampleFix":"// before\n$repo->expects($this->atLeastOnce())->method('save');\n$service->process($invalidDto); // validation fails, save() never called\n\n// after: exercise the path that actually saves\n$repo->expects($this->atLeastOnce())->method('save');\n$service->process($validDto);","handlingStrategy":"validation","validationCode":"// Cheap pre-check: make sure the SUT actually reaches the interaction by asserting\n// a state change or return value the call produces before relying on atLeastOnce().\n$this->assertNotNull($service->process($validDto)); // path that must call the collaborator","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Inject the mock into the SUT in the same statement where you create it to avoid wiring mistakes.","Double-check method() strings against the collaborator's interface; mocks of interfaces fail fast on typos only when called.","Use typed test doubles (createMock(Interface::class)) so a wrong method name surfaces as a config error, not a silent pass.","Cover the no-call branch with its own test so expectations are not blurred across paths."],"tags":["phpunit","mock","verification","never-invoked"],"backgroundTag":"mock-invocation-count-mismatch","analyzedSha":"f123cdb2a2d49f15025794166cfed8bda8627dd2","analyzedAt":"2026-08-23T01:20:58.058Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}