After an upgrade to PHPUnit 13, a green suite reports notices and deprecations from its mocks; what do they mean and how do you clear them?
answer
- notices do not fail by default
- No expectations were configured for the mock
- #[AllowMockObjectsWithoutExpectations]
- any() and with() without expects() deprecated
- --display-phpunit-notices shows details
basics
~10 sThey flag mocks that verify nothing: a mock with no expectations, expects($this->any()), or with() without expects(). Replace such mocks with createStub(), state a real count rule, or opt out with #[AllowMockObjectsWithoutExpectations].
solid answer
~40 sPHPUnit 13 separates stubs from mocks and reports mocks that do not use what makes them mocks. A mock that ends the test with neither a count rule nor an argument rule produces the notice "No expectations were configured for the mock object for ..."; switch it to `createStub()`, or add `#[AllowMockObjectsWithoutExpectations]` to the class or method when a shared fixture creates it. `expects($this->any())` is hard-deprecated since 12.5.5 and `with*()` without `expects()` since 13.0.2; both disappear in PHPUnit 14. These are PHPUnit notices and deprecations, so they do not fail the run unless the configuration asks them to; run with `--display-phpunit-notices` and `--display-phpunit-deprecations` to see which tests they come from, then fix them before the next major version turns them into errors.
code
php · 17 lines<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
final class SignUpServiceTest extends TestCase
{
private Mailer $mailer;
protected function setUp(): void
{
// Shared mock; only some tests add expects() on it
$this->mailer = $this->createMock(Mailer::class);
}
}go deeper
Recognise the no-expectations notice and know that swapping createMock() for createStub() is the usual fix.
Explain which configurations trigger each notice or deprecation and why any() does not count as an expectation.
Lead the cleanup: triage the reports, decide per test between stub and real rule, and make CI fail on new PHPUnit notices.
Time the cleanup against the PHPUnit 14 removals and set the suite-wide policy on opt-outs versus rewrites.
## Why a passing suite gets noisy PHPUnit 13 draws a hard line between a **test stub**, which only answers calls, and a **mock object**, which also verifies them. Older suites often used `createMock()` for every double, including ones that only supplied data. The runner now reports each place where a mock is used as a stub, or where an expectation is written so loosely that it checks nothing. These reports are **PHPUnit notices** and **PHPUnit deprecations**: issues about how the test is written, distinct from PHP's own deprecations raised by the code under test. ## The four messages you will meet | Message source | Trigger | Status in PHPUnit 13 | |---|---|---| | no-expectations notice | a `createMock()` double with no count rule and no argument rule | notice after the test | | `any()` deprecation | `expects($this->any())` | hard-deprecated since 12.5.5, removed in 14 | | `with*()` without `expects()` | `$mock->method('send')->with(...)` | hard-deprecated since 13.0.2 | | `atLeast(0)` deprecation | `atLeast()` with a non-positive argument | hard-deprecated since 13.0.2 | Note that `expects($this->any())` does not silence the first notice: PHPUnit does not count "any number of calls" as an invocation-count rule, so such a mock both triggers the deprecation and still counts as having no expectations. ## Clearing the no-expectations notice The full text is "No expectations were configured for the mock object for Mailer. Consider refactoring your test code to use a test stub instead. The #[AllowMockObjectsWithoutExpectations] attribute can be used to opt out of this check." Work through three cases: 1. **The double only supplies data.** Replace `$this->createMock(UserRepository::class)` with `self::createStub(UserRepository::class)`. Everything chained after `method()` keeps working, because stubs support the same `willReturn*()` answers. 2. **The call is the behaviour under test.** Add the real rule, for example `expects($this->once())->method('send')`, so the mock verifies something. 3. **A shared `setUp()` creates the mock and only some tests add expectations.** Put `#[AllowMockObjectsWithoutExpectations]` on the test class or on the specific methods. Prefer creating the mock inside the tests that verify it, since the attribute hides the signal for every test it covers. ## Clearing the deprecations - For `any()`: if the count genuinely does not matter, the double should be a stub; if it does, state it with `once()`, `exactly(n)`, `atLeastOnce()` or `atMost(n)`. - For `with()` without `expects()`: add the count rule in front, or drop `with()` and use a stub if you only wanted a return value. A `with()` without a count rule verified less than it appeared to. - For `atLeast(0)`: a lower bound of zero is satisfied even when the method is never called, so it verifies nothing; write the rule you mean, or use a stub. Two related soft deprecations need no immediate action but belong on the list: `id()` and `after()` for cross-method ordering, soft-deprecated since 13.1.0. ## Seeing and enforcing the reports By default a notice or deprecation from PHPUnit leaves the run green. Useful switches: - `--display-phpunit-notices` and `--display-phpunit-deprecations` print the details and the test each one came from; - the CLI option `--fail-on-phpunit-notice` makes a notice fail the run, which is how a team keeps a cleaned suite clean; - **sealing**: ending a mock's configuration chain with `seal()` (it lives on the builder that `method()` and `expects()` return) turns every unconfigured method into a zero-calls expectation and forbids further configuration, with "This test double has been sealed and cannot be configured further". It catches calls the test did not anticipate. ## What not to do - Do not blanket the suite with `#[AllowMockObjectsWithoutExpectations]` to make the output quiet; the attribute switches off the one signal that shows a mock verifying nothing. - Do not swap `any()` for `atLeast(0)`: the deprecation simply moves, and the rule still checks nothing. - Do not add `expects($this->once())` to a query just to satisfy the notice; that welds the test to how often the service reads data. - Do not confuse these reports with deprecations raised by the code under test; those come from PHP and are fixed in production code, not in the test. ## A sensible order of work 1. Run the suite once with both display flags and collect the messages. 2. Fix the no-expectations notices first; most are mechanical `createMock()` to `createStub()` changes. 3. Replace `any()` and loose `with()` calls, deciding per test whether a call count matters. 4. Turn on failing on PHPUnit notices in CI so the suite cannot drift back before PHPUnit 14 removes the deprecated APIs.
- Why does expects($this->any()) not silence the no-expectations notice?PHPUnit only treats a mock as having expectations when it carries a real invocation-count rule or an argument rule. A rule that accepts any number of calls is not counted, so the mock still triggers the notice, and `any()` adds its own deprecation on top.
- What does ending a PHPUnit 13 mock's configuration chain with seal() change?It freezes the double: any further `expects()` throws "This test double has been sealed and cannot be configured further", and every method you did not configure becomes an expectation of zero calls, so an unanticipated call fails the test.
saying these in an interview costs you the question
- PHPUnit notices fail the test run by default
- expects($this->any()) counts as a real expectation
- #[AllowMockObjectsWithoutExpectations] turns a mock into a stub
- The notices come from PHP itself, not from PHPUnit
- Switching createMock() to createStub() breaks willReturn() chains