@dataProvider 在 PHPUnit 12 静默失效:测试照跑,报的是 0 passed
现象与判据
PHPUnit 12 起,测试元数据只从 PHP attributes 读,docblock 注解支持已移除(11 里 deprecated,12 里删除)。升级后最常见的症状不是"注解不支持"的提示,而是测试照常被发现、照常执行,然后失败:
1) Tests\OldStyleTest::testAnnotation
ArgumentCountError: Too few arguments to function Tests\OldStyleTest::testAnnotation(), 0 passed in /path/vendor/phpunit/phpunit/src/Framework/TestCase.php on line 1320 and exactly 1 expected
PHPUnit 12.5 对 @dataProvider doc comment 完全不报错,只把它当作没有 provider,于是方法以 0 个参数调用。判据很直接:报 ArgumentCountError 且 0 passed,而方法签名上明明有参数——先查 provider 是不是还写在注释里。
正确写法:
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class SlugTest extends TestCase
{
public static function titles(): iterable
{
yield 'plain words' => ['Hello World', 'hello-world'];
yield 'punctuation' => ['Fix: PHP 8.5 (finally)!', 'fix-php-8-5-finally'];
yield 'only symbols' => ['?!?', 'untitled'];
}
#[DataProvider('titles')]
public function testMake(string $title, string $expected): void
{
$this->assertSame($expected, Slug::make($title));
}
}
失效点一:provider 必须 public static
非 static 的报错是:
There was 1 PHPUnit error:
1) Tests\NonStaticTest::testIt
The data provider Tests\NonStaticTest::values specified for Tests\NonStaticTest::testIt is invalid
Data Provider method Tests\NonStaticTest::values() is not static
同时该类还会报 No tests found in class,容易被误判成发现规则或命名空间问题。修法:加 static,去掉 provider 里的 $this。
失效点二:数据集个数与测试签名要对上
少给一个:
1) Tests\BrokenTest::testPair#0 with data ('only one value')
ArgumentCountError: Too few arguments to function Tests\BrokenTest::testPair(), 1 passed ... and exactly 2 expected
这条通常出现在有人改了测试方法签名但没改 provider。注意 PHPUnit 12.4 的 #6277 允许测试主动关掉"provider 提供值多于方法参数"的校验(PHP 本身允许多余实参),目的是让同一个 provider 被参数需求不同的多个测试方法复用;这是多给值的例外,少给值仍然是错误。
失效点三:provider 在 discovery 阶段执行
早于任何测试的 setUp。它不能连数据库、不能发网络请求;依赖状态的值应在测试方法体内计算。把容器启动放进 provider(static::bootKernel())看似能跑,但 KernelTestCase::tearDown() 会在第一个测试后关掉内核,provider 里 boot 出来的容器和测试里 boot 的容器状态可能不一致。12.4.4 修复了 provider 抛异常导致 PHPUnit 内部错误(#6408)的问题,但"能跑"不等于"该这么写"。
命名数据集
yield 用字符串 key。key 会进入输出和 --filter:
testMake with data set "only symbols"
未命名则显示 #0、#1。--testdox 输出同理(Make with data set "plain words")。失败时能直接看出是哪个用例,也能单独复跑。
内联数据与跨类 provider
#[TestWith(['A B', 'a-b'])]
#[TestWith([' trim me ', 'trim-me'], 'with spaces')]
#[TestWithJson('["x_y", "x-y"]')]
#[DataProviderExternal(SharedCases::class, 'empty')]
DataProviderExternal 指向另一个类的 static 方法,适合多个测试类共享同一组输入(比如所有 validator 都要拒绝的输入)。第一次用常遇到:
Class "Tests\SharedCases" does not exist
类文件在磁盘上却报不存在,是 composer 的 autoload-dev 没覆盖 tests/ 目录。--bootstrap vendor/autoload.php 不会猜目录映射,需要显式加 PSR-4:
"autoload-dev": { "psr-4": { "Tests\\": "tests/" } }
改完执行 composer dump-autoload。
迁移顺序
几百个 provider 的库也按这个顺序走:
grep -rn "@dataProvider\|@testWith" tests/先列出全部待改点。- 逐个改成
#[DataProvider('name')]并加use PHPUnit\Framework\Attributes\DataProvider;。 - 所有 provider 方法加
public static,删掉只读属性的$this->。 - provider 放在 helper 类里的,确认
autoload-dev覆盖该目录。 - 每步之后跑一次测试。第 2、3 步的错误长得很像(一个是
ArgumentCountError,一个是invalid data provider),分开跑才容易判断是哪类。
Rector 有对应 ruleset 可批量把注解转 attribute;Nextcloud 的 #53728 就是 +1513/−2888、479 个文件一次转完。
什么时候不该用 provider
数据集需要不同的 setUp、不同的 mock 或不同的断言时,它们其实是两个测试,拆成方法更可读。上千条生成数据用 yield 惰性生成能省内存,但失败输出会很吵,抽样更好。
12.x 后续几条硬弃用
顺手清掉:12.3 起 #[CoversNothing] 用在方法级、12.4 起 #[RunClassInSeparateProcess]、版本约束不带比较运算符(如 '8.3.0' 被当成精确匹配)都会报 deprecation,13 里会删。
参考:
- PHPUnit 12 发布公告:https://phpunit.de/announcements/phpunit-12.html
- Attributes 手册:https://docs.phpunit.de/en/12.5/attributes.html
- 仓库:https://github.com/sebastianbergmann/phpunit
- 弃用清单:https://github.com/sebastianbergmann/phpunit/blob/12.5/DEPRECATIONS.md
- ChangeLog 12.4:https://github.com/sebastianbergmann/phpunit/blob/12.4.5/ChangeLog-12.4.md