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.
Table of Contents
- 1. PHPUnit 11 at a glance: what is actually new
- 2. From docblock annotations to PHP attributes
- 3. Coverage directives in phpunit.xml: whitelist is gone
- 4. Mock system: changes and removed methods
- 5. New and removed assertion methods
- 6. phpunit.xml: schema changes and new directives
- 7. PHP requirements and type strictness
- 8. Step-by-step migration for existing projects
- 9. PHPUnit 10 vs. 11 in direct comparison
- 10. Summary
- 11. FAQ
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.