编程 @dataProvider 在 PHPUnit 12 静默失效:测试照跑,报的是 0 passed

2026-11-13 00:03:56

@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 的库也按这个顺序走:

  1. grep -rn "@dataProvider\|@testWith" tests/ 先列出全部待改点。
  2. 逐个改成 #[DataProvider('name')] 并加 use PHPUnit\Framework\Attributes\DataProvider;。
  3. 所有 provider 方法加 public static,删掉只读属性的 $this->。
  4. provider 放在 helper 类里的,确认 autoload-dev 覆盖该目录。
  5. 每步之后跑一次测试。第 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
复制全文 生成海报 PHPUnit PHP 单元测试 测试 数据提供者

推荐文章

程序员茄子在线接单