The development of PHPUnit towards clearer and more intention-focused testing practices reaches another milestone with version 13.0. Having written about testing with and without dependencies and the stub/mock intervention in PHPUnit 12.5, I would now like to highlight the logical consequence of this development: the deprecation of the any() matcher.
The philosophical basis
As explained in my previous articles, PHPUnit's modern testing philosophy is based on a clear conceptual separation of different types of test doubles. This distinction is not only academic in nature, but also has practical implications for the maintainability and comprehensibility of tests.
Test Stubs are used to control indirect inputs. They replace dependencies of the system under test (SUT) and provide defined return values without having to verify communication with these objects. A test stub is therefore a tool that provides the code under test with the necessary data.
Mock Objects, on the other hand, are observation points for indirect outputs. They verify whether and how the SUT communicates with its collaborators. A mock object is like a witness who logs whether and how the expected communication took place.
This dichotomy is fundamental to understanding why the any() matcher is an anti-pattern and is therefore being removed from PHPUnit.
A semantic dissonance
The any() matcher stands for "zero or more calls" to a method. However, this semantic meaning fundamentally conflicts with the purpose of mock objects. When I create a mock object with createMock() and then configure it with expects($this->any()), I am essentially saying: "I am creating a tool for verifying communication, but I do not care whether this communication actually takes place".
This statement is contradictory. Either I am interested in the communication – in which case I should use a specific matcher such as once(), exactly(2) or atLeast(1) – or I am not interested in it, in which case I should create a test stub with createStub() instead.
Consider the following problematic code:
$collaborator = $this->createMock(CollaboratingService::class); $collaborator ->expects($this->any()) ->method('doSomething') ->willReturn(true); $service = new Service($collaborator); $result = $service->doSomething(); $this->assertTrue($result);
What does this code actually test?
The use of any() signals that it does not matter whether the send() method is called or not. If that is the case, why are we using a mock object at all? We pay the price for the complexity of mock objects, but cannot benefit from their usefulness, namely the verification of communication between objects.
The historical context
The any() matcher is a child of its time, and that time is long gone. When PHPUnit first gained support for test doubles in 2006, there was only one method in the framework for creating test doubles: getMock(). This method was a general-purpose tool that could create both test stubs and mock objects without differentiating between these two fundamentally different concepts.
Anyone who needed a test stub, i.e. an object that was only supposed to provide data without having to verify communication, inevitably had to create a mock object with getMock(). The any() matcher was then a necessary trick: With expects($this->any()), we told the framework: "I do not expect anything from this mock object. Configure it as a test stub." This subsequently transformed the mock object into a test stub.
Fortunately, PHPUnit has evolved since then. Since PHPUnit 8.4, there is a method called createStub() that is specifically designed for test stubs. Since PHPUnit 12, this distinction has been technically enforced: expectations can finally no longer be configured on a createStub(). The any() matcher has thus become obsolete. What was once a necessary crutch is now a semantic error, an anti-pattern that expresses that the wrong tool was chosen for the job.
Since PHPUnit 12.5.5, the any() matcher has been soft-deprecated, and since PHPUnit 13, it has been hard-deprecated. Any test that uses it should instead be migrated to createStub() or extended with specific expectations.
The soft deprecation in PHPUnit 12.5 means that using any() does not yet result in a deprecation being issued during test execution. However, IDEs and static analysis tools can already warn developers. This phase gives teams time to review and migrate their code.
The hard deprecation in PHPUnit 13 means that using any() will result in a deprecation being issued during test execution. Tests will continue to work, but PHPUnit is making it clear that the any() matcher will be removed in PHPUnit 14.
The with() pitfall
The stricter separation between test stubs and mock objects has revealed a closely related anti-pattern involving the with() method. As reported in issue #6504, the combination of createMock() and with() without an explicit expects() call creates a subtle migration trap.
Consider the following code in PHPUnit 12.5:
$dependency = $this->createMock(MyInterface::class); $dependency ->method('myMethod') ->with('payload') ->willReturn('my result');
The code shown above uses createMock() but does not configure any expectations via expects(). Before version 12.5.11, PHPUnit 12.5 therefore emitted a notice: "No expectations were configured for the mock object for MyInterface."
The natural reaction is to follow this advice and switch to createStub():
$dependency = $this->createStub(MyInterface::class); $dependency ->method('myMethod') ->with('payload') ->willReturn('my result');
In PHPUnit 12.5, this works and the notice disappears. However, when upgrading to PHPUnit 13, this refactored code breaks because with() can no longer be called on a test stub.
The with() method configures argument expectations, which is a form of verification, and verification is the domain of mock objects, not test stubs.
As I explained in issue #6504, using with() without expects() is implicitly treated as if expects($this->any()) were used. This makes it a manifestation of the same anti-pattern described earlier in this article: a mock object is created, argument verification is configured, but the framework is told that it does not matter whether this communication actually takes place.
Using with() without expects() is deprecated since PHPUnit 13.0.2 and will no longer work in PHPUnit 14.
Additionally, to close the migration trap entirely, the use of with*() on test stubs has also been deprecated in PHPUnit 12.5.11. This prevents the scenario where developers follow the notice in PHPUnit 12.5 to switch from createMock() to createStub(), only to discover that their refactored tests break upon upgrading to PHPUnit 13.
The correct approach depends on the intent of the test:
- If the arguments passed to the method must be verified, use
createMock()together withexpects()andwith() - If the arguments do not need to be verified and the test double only needs to return a value, use
createStub()withoutwith()
This edge case reinforces the central message: each test must clearly express its intention.
The decision between a test stub and a mock object is not a mechanical refactoring, but a conscious choice about what the test is supposed to verify. When you find yourself using with(), you are expressing an expectation about how the SUT communicates with its dependency, and that is precisely what mock objects are for.
Best Practices
The following principles should be observed when writing new tests and revising existing tests:
- Intention before technique: Before writing a test, it should be clear what is to be tested. Is it about the behaviour of the SUT or the communication between objects?
-
Explicit expectations: Use specific matchers that express exactly what is expected:
once()is better thanany()because it communicates a clearer expectation. -
Test stubs for decoupling, mock objects for verification: The decision between
createStub()andcreateMock()must be made consciously. It documents the role of the test double in the test. -
Create test doubles locally: If possible, test doubles should be created in the test method itself and not in a before-test method such as
setUp(). This makes the dependencies of each test explicit and reduces unexpected interactions. -
Use
#[AllowMockObjectsWithoutExpectations]sparingly: The use of this attribute should be the exception, not the rule. If this attribute is needed frequently, it indicates structural problems in the test suite.
Looking ahead
With the removal of any() in PHPUnit 14, the separation between test stubs and mock objects will be fully enforced. It will then no longer be possible to accidentally or intentionally use a mock object as a test stub.
This strictness may seem restrictive at first, but it results in a test suite where each test clearly communicates its intention. New developers on the team will find the tests easier to understand, and maintenance will be simpler because the semantics are unambiguous.
The evolution of PHPUnit shows that a successful framework must not only add new features, but also have the courage to remove redundant functionality when it causes confusion or when better alternatives exist.