php进入 php 8.0 后单元测试失败,因为 "Unknown named parameter"

phpunit tests failing after going to php 8.0 because of "Unknown named parameter"

一旦我们需要的一些第三方库准​​备就绪,我们正准备迁移到 php 8.0.15。

我们用于单元测试的集中式 setUp() 函数处理我们 class 模拟的 constructorArg 填充。

当前使用 phpunit v9.5.14,我们得到失败的测试,响应为 Error : Unknown named parameter $User

我们没有在代码库中任何已知的地方使用命名参数。

if (empty($this->constructorArgs)) {
    $this->constructorArgs = array('User');
}
if (!empty($this->constructorArgs) && is_array($this->constructorArgs)) {
    foreach ($this->constructorArgs as $classname) {
        if (is_array($classname)) {
            $args[key($classname)] = current($classname);
            $classname = key($classname);
        } else {
            if ($classname == "Twig" || $classname == "Twig\Environment") {
                $args[$classname] = TwigFactory::mockTwig();
            } else {
                $args[$classname] = $this->getMockBuilder($classname)->disableOriginalConstructor()->getMock();
            }
        }
        $container->set($classname, $args[$classname]);
    }
}

$this->mock = $this->getMockBuilder($this->class)
    ->setMethods($this->methods)
    ->setConstructorArgs($args)
    ->getMock();   <-- Error states this line, unfortunately no stack trace

constructorArgs 像这样填充到设置中:

$this->constructorArgs = array('User','AnotherClass', 'YetAnother');

我们已经尝试了我们能想到的一切,认为这可能与 class 结构中变量名的大小写有关,即“User $user”,但到目前为止还没有解决了这个问题。

在我们开始之前,让我们创建一个 minimal, reproducible example:

class User {}

class Example {
     public User $user;

     public function __construct(User $user) {
          $this->user = $user;
     }
}

class ExampleTest extends PHPUnit\Framework\TestCase {
     public function testExample() {
          $args = [];
          $args['User'] = $this->getMockBuilder('User')->disableOriginalConstructor()->getMock();

          $mock = $this->getMockBuilder(Example::class)
                ->setConstructorArgs($args)
                ->getMock();

          $this->assertTrue(true);
      }
}

运行PHP7.4和PHPUnit 9.5.14,这个通过;使用 PHP 8.0 和相同的库,它给出了您报告的错误:

Error: Unknown named parameter $User

实际上,我们可以进一步简化:我们可以只说 $args['User'] = new User; 而不是 $args['User'] = $this->getMockBuilder('User')->disableOriginalConstructor()->getMock(); 并得到相同的错误。

现在,让我们看看我们在做什么:

  1. 我们创建一个关联数组,将 class 名称映射到(模拟)对象
  2. 我们将该关联数组传递给 Mock Builder 的 setConstructorArgs 方法
  3. 奇迹发生了...

那么, 会发生什么?也许 the source of PHPUnit 会给出一些线索。

好吧,setConstructorArgs just sets a property, which is used in getMock, then passed through a bunch of different methods; eventually, it ends up passed to MockObject\Generator::getObject,如果我们去掉一些错误处理,这样做:

$class = new ReflectionClass($className);
$object = $class->newInstanceArgs($arguments);

那么,让我们看看是否可以使用它来为我们的问题制作一个更小的例子

class User {}

class Example {
     public User $user;

     public function __construct(User $user) {
          $this->user = $user;
     }
}

$class = new ReflectionClass(Example::class);
$object = $class->newInstanceArgs(['User' => new User]);

由于这是 self-contained 代码,我们可以使用方便的在线工具 https://3v4l.org to compare output in different PHP versions: https://3v4l.org/QU4jS

不出所料,PHP7.4满意,PHP8.0及以上报错:

Fatal error: Uncaught Error: Unknown named parameter $User in /in/QU4jS:14
Stack trace:
#0 /in/QU4jS(14): ReflectionClass->newInstanceArgs(Array)
#1 {main}
  thrown in /in/QU4jS on line 14

那么,这是怎么回事?好吧,the manual page for ReflectionClass::newInstanceArgs 并没有(目前)说太多关于提供的数组应该是什么样子,或者命名参数支持,但我们可以做出有根据的猜测:它试图将我们的关联数组作为命名参数匹配到构造函数.以前的版本,因为它们没有命名参数,所以简单地忽略键并按顺序应用参数。

我们可以很容易地通过构造一个带有两个参数的 class 来测试这个理论:

class Example2 {
    public function __construct($first, $second) {
        echo "$first then $second\n";
    }
}

$class = new ReflectionClass(Example2::class);
$object = $class->newInstanceArgs(['second' => 'two', 'first' => 'one']);

run on multiple versions 我们可以看到旧版本的 PHP 根据数组的 order 输出“二然后一”;和较新的版本根据数组的 keys 输出“一然后二”。


所以,长话短说,解决方法是什么?很简单,不要在构造函数参数数组中使用键:

class ExampleTest extends PHPUnit\Framework\TestCase {
     public function testExample() {
          $args = [];
          $args[] = new User;

          $mock = $this->getMockBuilder(Example::class)
                ->setConstructorArgs($args)
                ->getMock();

          $this->assertTrue(true);
      }
}

如果您需要在设置逻辑期间使用它们来跟踪事物,只需在传入它们时使用 array_values 丢弃它们:

class ExampleTest extends PHPUnit\Framework\TestCase {
     public function testExample() {
          $args = [];
          $args['User'] = new User;

          $mock = $this->getMockBuilder(Example::class)
                ->setConstructorArgs(array_values($args))
                ->getMock();

          $this->assertTrue(true);
      }
}