由于“未知的命名参数”,phpunit 测试在进入 php 8.0 后失败

dun*_*koh 6 php php-8

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

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

当前使用 phpunit v9.5.14,我们得到失败的测试,响应错误:未知的命名参数 $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
Run Code Online (Sandbox Code Playgroud)

constructorArgs 被填充到设置中,如下所示:

$this->constructorArgs = array('User','AnotherClass', 'YetAnother');
Run Code Online (Sandbox Code Playgroud)

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

IMS*_*SoP 8

在开始之前,让我们创建一个最小的、可重现的示例

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);
      }
}
Run Code Online (Sandbox Code Playgroud)

使用 PHP 7.4 和 PHPUnit 9.5.14 运行,可以通过;使用 PHP 8.0 和相同的库,它会给出您报告的错误:

错误:未知的命名参数 $User

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

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

  1. 我们创建一个关联数组,将类名映射到(模拟)对象
  2. 我们将该关联数组传递给模拟构建器的setConstructorArgs方法
  3. 一些魔法发生了......

那么,发生什么呢?也许PHPUnit 的源码会提供一些线索。

好吧,setConstructorArgs只需设置一个属性,该属性在 中使用getMock,然后通过一堆不同的方法传递;最终,它最终传递给MockObject\Generator::getObject,如果我们去掉一些错误处理,它会执行以下操作:

$class = new ReflectionClass($className);
$object = $class->newInstanceArgs($arguments);
Run Code Online (Sandbox Code Playgroud)

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

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]);
Run Code Online (Sandbox Code Playgroud)

由于这是独立的代码,我们可以使用https://3v4l.org上方便的在线工具来比较不同 PHP 版本的输出: https: //3v4l.org/QU4jS

正如预期的那样,PHP 7.4 对此感到满意,PHP 8.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
Run Code Online (Sandbox Code Playgroud)

那么,这是怎么回事?好吧,手册页ReflectionClass::newInstanceArgs(目前)并没有详细说明所提供的数组应该是什么样子,或者命名参数支持,但我们可以做出有根据的猜测:它试图将我们的关联数组作为命名参数与构造函数进行匹配。以前的版本由于没有命名参数,因此只是忽略这些键并按顺序应用参数。

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

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 Code Online (Sandbox Code Playgroud)

在多个版本上运行时,我们可以看到旧版本的 PHP 根据数组的顺序输出“二然后一”;较新的版本根据数组的键输出“一然后二”。


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

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

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

          $this->assertTrue(true);
      }
}
Run Code Online (Sandbox Code Playgroud)

如果您需要在设置逻辑期间使用它们来跟踪事物,只需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);
      }
}
Run Code Online (Sandbox Code Playgroud)