PHPUnit 11: What Changed and What Teams Need to Adjust
AI generated
@test
assert
PHPUnit 11 · Migration · Breaking Changes · PHP 8.1+
PHPUnit 11: What changed
and what teams need to adjust

PHPUnit 11 is not an incremental update. It removes docblock annotations in favor of native PHP attributes, tightens coverage configuration, fundamentally changes the mock system, and sets PHP 8.1 as the minimum requirement. Anyone migrating has to change more than just the version number in composer.json.

14 min read Annotations to attributes · coverage · mocks · phpunit.xml PHPUnit 10 to 11 · PHP 8.1+

1. PHPUnit 11 at a glance: what is actually new

PHPUnit 11 continues the cleanup that started with PHPUnit 10: everything that was marked deprecated is now removed. This mainly affects the docblock annotation system, which was PHPUnit's primary metadata system for over twenty years. @test, @dataProvider, @depends, @covers, and @group, all these annotations are no longer recognized in PHPUnit 11 once strict attribute validation is enabled. Instead, PHPUnit relies on native PHP attributes, which have been available since PHP 8.0.

Besides the annotations, several classes and methods have been removed from the public API. TestCase::createMock() still exists, but the way mock objects are configured has changed. withConsecutive() has been completely removed, a method that was used heavily in many legacy test suites. That often requires structural changes to tests, not just syntactic ones.

The upside: PHPUnit 11 is faster, stricter, and fails earlier on misconfigured tests instead of silently delivering wrong results. Teams performing the migration regularly report discovering real bugs in tests that had previously gone unnoticed, because PHPUnit 11 no longer accepts silent fallbacks for misconfigured mocks or faulty assertions.

2. From docblock annotations to PHP attributes

The biggest visible change in PHPUnit 11 is the shift from docblock annotations to PHP attributes. Instead of /** @test */ you write #[Test] above the method. Instead of /** @dataProvider provideData */ you write #[DataProvider('provideData')]. This change is not merely syntactic: PHP attributes are processed by the parser, are type-safe, and are fully understood by IDEs like PhpStorm, including auto-completion and refactoring support.

Especially important: #[CoversClass(MyClass::class)] and #[CoversMethod(MyClass::class, 'method')] replace the @covers annotation. These attributes provide precise coverage mapping: PHPUnit now knows at compile time (not at runtime) which class a test is supposed to cover. That makes coverage reports more reliable and prevents false-positive coverage numbers caused by unintended side effects.


<?php
// BEFORE: PHPUnit 10 with docblock annotations
use PHPUnit\Framework\TestCase;

class OrderServiceTest extends TestCase
{
    /**
     * @test
     * @covers \App\Service\OrderService::calculateTotal
     * @dataProvider provideOrderData
     */
    public function it_calculates_order_total(array $items, float $expected): void
    {
        // ...
    }

    public function provideOrderData(): array { /* ... */ }
}

// AFTER: PHPUnit 11 with PHP attributes
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

#[CoversClass(\App\Service\OrderService::class)]
class OrderServiceTest extends TestCase
{
    #[Test]
    #[DataProvider('provideOrderData')]
    public function itCalculatesOrderTotal(array $items, float $expected): void
    {
        // ...
    }

    public static function provideOrderData(): array { /* ... */ }
}

3. Coverage directives in phpunit.xml: whitelist is gone

In phpunit.xml, the <whitelist> directive has been deprecated since PHPUnit 10 and is completely removed in PHPUnit 11. It is replaced by the <source> directive with <include> and <exclude> elements. The semantics stay the same, which files are included in coverage analysis, but the XML structure has changed. Anyone still using the old configuration gets no coverage reports at all in PHPUnit 11.

Also new: the requireCoverageMetadata="true" directive on the <phpunit> element. When set, PHPUnit fails if a test has no #[CoversClass] or #[CoversMethod] attribute. This forces teams to make coverage mappings explicit. In large projects it is advisable to start with requireCoverageMetadata="false" and migrate step by step. The beStrictAboutCoverageMetadata attribute controls whether missing metadata is treated as a warning or an error.

4. Mock system: changes and removed methods

The biggest migration hurdle for many teams is the removal of withConsecutive(). This method allowed configuring a mock to return different values on consecutive calls. In PHPUnit 11 there is no direct replacement, instead the logic has to be restructured. The recommended approach is using willReturnCallback() with an internal counter, or using willReturnOnConsecutiveCalls(), which only works for return values without argument checking.

Also changed: getMockBuilder() no longer accepts strings for method names that do not exist. PHPUnit 11 is stricter about mock configuration. If you mock a non-existent method, PHPUnit throws an exception instead of silently creating the mock. This uncovers misconfigured mocks that in PHPUnit 10 quietly produced faulty tests.


<?php
// REMOVED in PHPUnit 11: withConsecutive()
$mock->method('find')
     ->withConsecutive([1], [2], [3])
     ->willReturnOnConsecutiveCalls($order1, $order2, $order3);

// REPLACEMENT with willReturnCallback():
$callCount = 0;
$returns = [$order1, $order2, $order3];
$mock->method('find')
     ->willReturnCallback(function (int $id) use (&$callCount, $returns) {
         return $returns[$callCount++] ?? null;
     });

// ALTERNATIVE for simple cases without argument checking:
$mock->method('find')
     ->willReturnOnConsecutiveCalls($order1, $order2, $order3);

// New: createMockForIntersectionOfInterfaces() for intersection types
$mock = $this->createMockForIntersectionOfInterfaces([
    \Countable::class,
    \Iterator::class,
]);

5. New and removed assertion methods

PHPUnit 11 removes several assertion methods that were marked deprecated in earlier versions. assertFileNotExists() is now called assertFileDoesNotExist(). assertNotEmpty() still exists, but the negating variants with a "Not" prefix have become more consistent. Method names now consistently follow the pattern assertXxx() for positive assertions and assertXxxDoesNotExist() for negative ones, more readable and unambiguous than the old assertNotXxx() forms.

Newly added are assertions for modern PHP features. assertIsEnum() checks whether a value is a PHP 8.1 enum instance. assertObjectHasProperty() replaces the cumbersome workaround with assertObjectHasAttribute(), which was deprecated in PHP 8.2. Teams testing PHP 8.1 features like enums, fibers, and readonly properties find matching assertions in PHPUnit 11 instead of having to make do with generic assertions.

6. phpunit.xml: schema changes and new directives

The phpunit.xml has changed structurally in PHPUnit 11. The XSD schema is stricter, elements and attributes that were tolerated in earlier versions now produce validation errors. The simplest way to migrate the configuration is the vendor/bin/phpunit --migrate-configuration command, which automatically adapts the existing phpunit.xml to the new structure. This covers most syntactic changes, but not content changes such as the coverage directives.

New directives in PHPUnit 11: executionOrder="depends,defects" runs tests with dependencies first and prioritizes tests that failed most recently. displayDetailsOnTestsThatTriggerWarnings="true" shows detailed information when tests trigger PHP warnings. This is especially relevant for PHP 8.4 projects, where many outdated patterns now produce deprecation warnings, PHPUnit 11 makes these visible instead of ignoring them.


<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml - PHPUnit 11 configuration -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         cacheDirectory=".phpunit.cache"
         executionOrder="depends,defects"
         requireCoverageMetadata="false"
         beStrictAboutCoverageMetadata="true"
         displayDetailsOnTestsThatTriggerWarnings="true"
         displayDetailsOnTestsThatTriggerDeprecations="true">

  <!-- NEW: <source> instead of <whitelist> -->
  <source>
    <include>
      <directory suffix=".php">src</directory>
    </include>
    <exclude>
      <directory>src/Migrations</directory>
      <file>src/Kernel.php</file>
    </exclude>
  </source>

  <coverage>
    <report>
      <html outputDirectory="build/coverage/html"/>
      <clover outputFile="build/coverage/clover.xml"/>
      <text outputFile="php://stdout" showUncoveredFiles="false"/>
    </report>
  </coverage>
</phpunit>

7. PHP requirements and type strictness

PHPUnit 11 sets PHP 8.1 as the minimum requirement. That means projects still supporting PHP 7.4 or PHP 8.0 cannot migrate to PHPUnit 11 without also raising the minimum PHP version at the same time. For most active projects PHP 8.1 is a given by now, but in legacy systems and Magento projects with older infrastructure, the PHP version has to be raised first.

Internally, PHPUnit 11 consistently uses PHP 8.1 features: enums for test status and result types, readonly properties for immutable configuration objects, and intersection types for mock factories. This makes PHPUnit 11 internally more maintainable and stable, but has no direct impact on test code, except that these features can now be used freely in your own tests too, and PHPUnit ships with matching assertions for them.

8. Step-by-step migration for existing projects

The recommended migration path starts with PHPUnit 10 as an intermediate step: first eliminate all deprecation warnings in PHPUnit 10, these are exactly the changes that become errors in PHPUnit 11. Replace annotations with attributes, whitelist with source, withConsecutive() with willReturnCallback(). Only once the test suite runs on PHPUnit 10 without deprecation warnings should you update to PHPUnit 11.

For large test suites, an automated migration of the annotations is recommended. The Composer package rector/rector with the ruleset configuration for PHPUnit attributes can convert most annotations to attributes automatically. Manual follow-up work remains necessary for withConsecutive() and for tests with complex @covers patterns that cannot be translated directly to class-level attributes.

Feature PHPUnit 10 PHPUnit 11 Migration effort
Test marking /** @test */ (deprecated) #[Test] Automatable with Rector
Coverage config <whitelist> (deprecated) <source> --migrate-configuration
Consecutive mocks withConsecutive() (deprecated) willReturnCallback() Manual, effort-intensive
Data provider @dataProvider (deprecated) #[DataProvider('...')] Automatable with Rector
Minimum PHP version PHP 8.1 PHP 8.1 No difference

10. Summary

PHPUnit 11 is a significant update that consistently breaks with legacy patterns. The shift from docblock annotations to PHP attributes makes tests more readable and IDE-friendly. The new coverage directives in phpunit.xml are more clearly structured. The stricter mock system uncovers misconfigured tests that used to produce silent bugs. The migration effort is real, but manageable, especially when Rector automates the annotation migration.

Teams on PHP 8.1 or higher should actively tackle the migration to PHPUnit 11. The investment pays off through better coverage quality, more precise error messages, and a test framework that fully supports modern PHP features. The recommended path: first eliminate all PHPUnit 10 deprecations, then upgrade to 11, run --migrate-configuration, and perform the mock refactorings manually.

Mironsoft

PHPUnit migration, test infrastructure and code quality for PHP teams

Need a PHPUnit 11 migration for your project?

We analyze your existing test suite, identify all breaking changes, and carry out the migration from PHPUnit 10 to 11, with Rector automation and manual mock refactoring support.

Analysis

Identify all deprecations and breaking changes in the existing test suite

Automation

Configure and run Rector rules for annotation-to-attribute migration

Manual migration

Manually adjust withConsecutive() refactorings and coverage directives

PHPUnit 11 migration, the essentials at a glance

Annotations to attributes

@test to #[Test], @dataProvider to #[DataProvider('...')]. Rector automates most conversions.

Coverage configuration

<whitelist> to <source> with <include> and <exclude>. Command: --migrate-configuration.

withConsecutive() is gone

Replace with willReturnCallback() using an internal counter, or willReturnOnConsecutiveCalls() without argument checking.

Migration strategy

Eliminate PHPUnit 10 deprecations first, then upgrade to 11. Rector, then manual mocks, then phpunit.xml, then CI test run.

11. FAQ: PHPUnit 11 migration

1Which PHP version does PHPUnit 11 require?
PHP 8.1 at minimum. PHPUnit 10 can still be used on PHP 8.0 as an intermediate step. Upgrade PHP first, then migrate PHPUnit.
2Can I use annotations and attributes at the same time?
In PHPUnit 10, yes, in PHPUnit 11, no longer by default. Recommendation: migrate completely, do not mix.
3How do I replace withConsecutive() in PHPUnit 11?
With willReturnCallback() and an internal counter. For simple cases without argument checking: willReturnOnConsecutiveCalls().
4What does --migrate-configuration do?
Automatically converts phpunit.xml: whitelist to source, removes deprecated attributes, updates the XSD. Saved back directly.
5requireCoverageMetadata vs. beStrictAboutCoverageMetadata?
require fails, beStrict warns. During migration: enable beStrict first, switch to require after the migration is complete.
6Can Rector automate the annotation migration?
Yes. Rector automatically converts @test, @dataProvider, @depends, @covers into PHP attributes. withConsecutive() must be refactored manually.
7How does the data provider change in PHPUnit 11?
Data provider methods must now be static: public static function provideData(). Non-static methods produce an error.
8Are there new assertions in PHPUnit 11?
assertIsEnum() for PHP 8.1 enums, assertObjectHasProperty() as a replacement for the deprecated assertObjectHasAttribute().
9What is the recommended migration path?
PHPUnit 10, eliminate deprecations, Rector, manual withConsecutive(), --migrate-configuration, PHPUnit 11, run through CI.
10Why was withConsecutive() removed?
Design problem: argument expectations were not validated clearly. Misconfigured mocks never failed. Removing it forces more explicit mock design.