The image shows a person untangling a complicated thought in order to find a solution to a problem.

I originally sent this article to my subscribers as a newsletter on February 6, 2026. See the end of this article for details.

PHPUnit 13

PHPUnit 13 marks a significant milestone in the evolution of PHP's most widely adopted testing framework. This new major version, released on February 6, 2026, embodies a proven development philosophy: every February, a new major version of PHPUnit is released that serves as the annual checkpoint for eliminating technical debt.

This release cycle provides a strategic opportunity to remove deprecated functionality that has reached the end of its support window, as well as discontinuing support for PHP versions that are no longer actively maintained by the PHP project itself.

PHPUnit 13 introduces significant changes to the baseline requirements by dropping support for PHP 8.3 and establishing PHP 8.4 as the minimum supported version. This aligns with PHP's own support lifecycle, ensuring that PHPUnit users benefit from the latest language features.

It is important to note, however, that the release of PHPUnit 13 does not render earlier versions obsolete immediately, as they will continue to function in existing projects. While these older versions will eventually cease to receive bug fixes, I am committed to maintaining their compatibility with newly released PHP versions for as long as possible.

The architecture of PHPUnit's release cycle deliberately separates the responsibilities of major and minor versions:

Major releases, delivered annually in February, provide the foundational infrastructure for the year ahead. They focus primarily on maintenance, such as cleaning the codebase by removing deprecated APIs and ensuring PHP version compatibility.

Minor releases, shipped throughout the remainder of the year, deliver new features and capabilities that extend PHPUnit's functionality.

Although major releases typically lack flashy new additions, PHPUnit 13 is a notable exception. This release introduces several new features, at least one of which warrants detailed examination as it represents a meaningful advancement in how we approach testing in modern PHP applications.

The legacy problem

PHPUnit's support for test doubles has long provided developers with powerful tools with which to verify interactions between objects. One such tool was the withConsecutive() method, which enabled developers to specify that a mocked method should be called multiple times, with different parameters for each invocation. However, this method had fundamental design limitations, which led to its deprecation in PHPUnit 9.6 and subsequent removal in PHPUnit 10.

The core issue stemmed from the rigid sequential matching requirement of withConsecutive() and its inability to handle dynamic scenarios where parameter sets might arrive in an unpredictable order or where only partial matching was necessary.

When upgrading to PHPUnit 10, developers found themselves having to implement cumbersome workarounds involving willReturnCallback() and complex closure logic. This often required dozens of lines of boilerplate code to replicate simple consecutive call verification scenarios.

A better solution

The need for more sophisticated parameter matching capabilities was articulated in issue #6407, leading to the implementation of two specialised rules in PHPUnit's mock object system as detailed in pull request #6455. These new rules offer greater control over how mock objects validate parameter sets across multiple invocations, eliminating the issues associated with withConsecutive().

The examples shown below use Dispatcher interface as well as the Service, AnEvent, and AnotherEvent classes from issue #6407.

Expecting parameter sets in a specific order

The withParameterSetsInOrder() rule addresses the original use case of withConsecutive(), offering enhanced reliability and clearer semantics. It verifies that a method receives parameters in a specific sequence, with each invocation being matched against a predefined set of parameters in the exact order they are specified in:

$dispatcher = $this->createMock(Dispatcher::class);

$dispatcher
    ->expects($this->exactly(2))
    ->method('dispatch')
    ->withParameterSetsInOrder(
        [new AnEvent],
        [new AnotherEvent],
    );

$service = new Service($dispatcher);

$service->doSomething();

The key improvement lies in the rule's internal implementation, which robustly maintains invocation state and provides clearer failure messages when expectations are violated. Rather than relying on fragile array indexing, the rule uses a dedicated matcher to track invocation count and parameter validation separately. This eliminates the race conditions and state mutation issues that afflicted withConsecutive().

Expecting parameter sets in any order

The withParameterSetsInAnyOrder() rule introduces a new capability to the mocking system in PHPUnit. It verifies that all specified parameter sets are used during test execution, regardless of their order:

$dispatcher = $this->createMock(Dispatcher::class);

$dispatcher
    ->expects($this->exactly(2))
    ->method('dispatch')
    ->withParameterSetsInAnyOrder(
        [new AnotherEvent],
        [new AnEvent],
    );

$service = new Service($dispatcher);

$service->doSomething();

This addresses scenarios in which asynchronous operations, parallel processing, or non-deterministic execution flows would make sequential verification using withParameterSetsInOrder() impractical.

Using withParameterSetsInOrder() when the order is not relevant creates unnecessary coupling between the test code and the implementation details. This manifests as test brittleness: refactoring the production code to reorder operations without changing the observable behaviour will cause the tests to fail, even though the functionality remains correct. Such failures generate false negatives, which erode confidence in the test suite and increase the maintenance burden.

By contrast, withParameterSetsInAnyOrder() focuses verification on what matters, that all expected interactions occurred, while remaining resilient to implementation changes. This approach aligns with the testing principle of verifying behaviour rather than implementation, producing tests that serve as reliable regression detectors without imposing artificial constraints on code evolution. Reserve withParameterSetsInOrder() for scenarios where sequential execution in a specific order is intrinsic to correctness.

Implementation details

The rules integrate with PHPUnit's existing InvocationMocker infrastructure through a clean extension point. Each rule implements the ParameterSetMatcher interface. This design allows the rules to participate in PHPUnit's verification phase, where they can assert that all expected parameter sets were consumed appropriately.

The implementation addresses several technical debt items from the withConsecutive() era. For example, each rule instance maintains independent state and failure messages specify exactly which parameter set failed to match and at which invocation index.

Migration path

The migration path is straightforward for developers currently using callback-based workarounds. The example shown below uses the most common pattern, which involves using willReturnCallback() with embedded assertions:

<?php declare(strict_types=1);
$dispatcher = $this->createMock(Dispatcher::class);

$matcher = $this->exactly(2);

$expectedCalls = [
    [new AnEvent],
    [new AnotherEvent]
];

$dispatcher
    ->expects($matcher)
    ->method('dispatch')
    ->willReturnCallback(
        function ($event) use ($matcher, $expectedCalls)
        {
            $invocationIndex = $matcher->numberOfInvocations() - 1;
            $expectedEvent   = $expectedCalls[$invocationIndex][0];

            $this->assertEquals(
                $expectedEvent,
                $event,
                sprintf(
                    'Expected event at invocation %d does not match',
                    $invocationIndex + 1,
                ),
            );
        }
    );

$service = new Service($dispatcher);

$service->doSomething();

When refactored to use withParameterSetsInOrder() instead of willReturnCallback(), the example shown above looks exactly like the example for withParameterSetsInOrder() which I showed you earlier.

Ecosystem impact

These new matchers significantly enhance PHPUnit's mocking capabilities. The rule-based approach creates extension points for custom parameter matching logic, enabling teams to define domain-specific validation rules that integrate seamlessly with PHPUnit's test double functionality.

Eliminating callback workarounds reduces technical debt and improves test performance by avoiding the overhead of closure execution and manual assertion management. This feature also removes a significant barrier to the adoption of PHPUnit 10 and newer versions, as the removal of the withConsecutive() method had been repeatedly identified as a primary obstacle.

The introduction of withParameterSetsInOrder() and withParameterSetsInAnyOrder() is more than just an API replacement; it establishes a foundation for next-generation mocking features that prioritise expressiveness, reliability, and maintainability in modern PHP testing practices.

Thank you, Christina!

The new parameter matching capabilities discussed in this article were implemented by Christina Koenig. During one of my training sessions, she posed a thoughtful question that directly inspired issue #6407, highlighting a genuine pain point in the testing workflow that many developers had been struggling with since the removal of withConsecutive().

Rather than simply accepting the existing workarounds, Christina took the initiative to delve into PHPUnit's internals after the training concluded. Working independently, she studied the test double functionality's implementation details, familiarised herself with the codebase's design patterns, and ultimately developed a robust solution in pull request #6455.

Her contribution demonstrates not only technical proficiency but also the kind of self-directed learning and problem-solving that drives Open Source forward. Christina's work will directly improve the testing experience for all PHPUnit users, transforming a commonly cited migration barrier into an opportunity for enhanced functionality.

Livestream

Here is the recording of the livestream in which I explained the most important changes in PHPUnit 13:

PHPUnit 13.0

Exclusive insights delivered to your inbox

Coinciding with each PHPUnit feature release every two months, I deliver a comprehensive analysis of new features, implementation details, and the strategic reasoning behind every enhancement directly to subscribers. It's more than just a ChangeLog: it's your gateway to understanding how cutting-edge testing capabilities can transform your development workflow.

I publish the content of the newsletter here on this website a month after my subscribers have received it. Subscribe now to ensure you receive valuable information about PHPUnit as soon as possible.