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();
并得到相同的错误。
现在,让我们看看我们在做什么:
- 我们创建一个关联数组,将 class 名称映射到(模拟)对象
- 我们将该关联数组传递给 Mock Builder 的
setConstructorArgs
方法
- 奇迹发生了...
那么, 会发生什么?也许 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);
}
}
一旦我们需要的一些第三方库准备就绪,我们正准备迁移到 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();
并得到相同的错误。
现在,让我们看看我们在做什么:
- 我们创建一个关联数组,将 class 名称映射到(模拟)对象
- 我们将该关联数组传递给 Mock Builder 的
setConstructorArgs
方法 - 奇迹发生了...
那么, 会发生什么?也许 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);
}
}