{"record":{"id":"b936f8378f5e7148","repo":"sebastianbergmann/phpunit","slug":"s-was-expected-to-be-s-but-was-s","errorCode":null,"errorMessage":"%s was expected to be %s but was %s.","messagePattern":"(.+?) was expected to be (.+?) but was (.+?)\\.","errorType":"exception","errorClass":"ExpectationFailedException","httpStatus":null,"severity":"error","filePath":"src/Framework/MockObject/Runtime/Matcher.php","lineNumber":230,"sourceCode":"        }\n\n        try {\n            $this->invocationRule->verify();\n        } catch (ExpectationFailedException) {\n            $actual = $this->invocationRule->numberOfInvocations();\n\n            if ($actual === 0) {\n                $invoked = 'never invoked';\n            } elseif ($actual === 1) {\n                $invoked = 'invoked once';\n            } else {\n                $invoked = sprintf(\n                    'invoked %d times',\n                    $actual,\n                );\n            }\n\n            throw new ExpectationFailedException(\n                sprintf(\n                    '%s was expected to be %s but was %s.',\n                    $this->methodNameRule->failureDescription($this->className),\n                    $this->invocationRule->toString(),\n                    $invoked,\n                ),\n            );\n        }\n\n        if ($this->parametersRule === null) {\n            $this->parametersRule = new AnyParameters;\n        }\n\n        $invocationIsAny    = $this->invocationRule instanceof AnyInvokedCount;\n        $invocationIsNever  = $this->invocationRule instanceof InvokedCount && $this->invocationRule->isNever();\n        $invocationIsAtMost = $this->invocationRule instanceof InvokedAtMostCount;\n\n        if (!$invocationIsAny && !$invocationIsNever && !$invocationIsAtMost) {","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/sebastianbergmann/phpunit/blob/f123cdb2a2d49f15025794166cfed8bda8627dd2/src/Framework/MockObject/Runtime/Matcher.php#L212-L248","documentation":"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'.","triggerScenarios":"->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.","commonSituations":"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).","solutions":["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.","If an exception legitimately aborts the flow, use expectException() and change the expectation to ->never() or remove it.","Verify you are asserting on the same mock instance the subject actually uses (no new mock created inside the subject).","For call-count changes from retries/loops, model them explicitly with exactly()/atLeast() based on the real contract."],"exampleFix":"// before\n$logger = $this->createMock(Logger::class);\n$logger->expects($this->once())->method('persist');\n$service->run($logger); // run() returns early, never persists\n\n// after\n$logger->expects($this->never())->method('persist');\n$service->run($logger);","handlingStrategy":"validation","validationCode":"// make the contract explicit and cheap to check: list expected calls before acting\n$this->expectedCalls = ['save' => 1];\n// after acting, cheap pre-verification is not possible generically —\n// instead keep expectations minimal and derived from the documented behavior","typeGuard":"// interrogate the double before verify(): cheap smoke check in helpers\n// (internal API) count invocations that already happened\n$handler = $mock->__phpunit_getInvocationHandler();\n// use only for debugging test scaffolding, not production logic","tryCatchPattern":"try {\n    $mock->__phpunit_verify(); // or let the TestCase run verification\n} catch (PHPUnit\\Framework\\ExpectationFailedException $e) {\n    // count mismatch: read 'was expected to be invoked once but was never invoked',\n    // adjust the rule (never/any/exactly) or fix the subject's control flow\n}","preventionTips":["Derive expectations from documented behavior, not implementation details; count-based tests are brittle.","When the subject may abort early, assert the exception first and relax call-count expectations.","Double-check that the subject received the same mock instance you configured."],"tags":["phpunit","mock-object","expects","invocation-count","expectation-failed"],"backgroundTag":"mock-invocation-count-mismatch","analyzedSha":"f123cdb2a2d49f15025794166cfed8bda8627dd2","analyzedAt":"2026-08-23T01:20:58.058Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}