PhpUnit 没有用字符串数字和摘要识别 TypeError Class

PhpUnit does not identify TypeError with string numbers and Abstract Class

与以下class

declare(strict_types=1);

abstract class IntValueObject
{
    public function __construct(protected int $value)
    {
    }
}

和测试

declare(strict_types=1);

final class IntValueObjectTest extends TestCase
{
    public function testWithNotValidValue(): void
    {
        $value = '1';
        $this->expectException(\TypeError::class);
        $this->getMockForAbstractClass(IntValueObject::class, [$value]);
    }
}

return

  1. Api\Tests\Shared\Domain\ValueObject\IntValueObjectTest::testWithNotValidValue Failed asserting that exception of type "TypeError" is thrown.

如果我将 $value 从 '1' 更改为 'foo' 如果它通过了测试。

我们使用 PHP 8,在生产中,如果传递值 '1' 会给出 TypeError,为什么在测试中没有发生这种情况?

提前致谢。


“问题”的根源

https://bugs.php.net/bug.php?id=81258

可能的解决方案

declare(strict_types=1);

final class IntValueObjectTest extends TestCase
{
    public function testWithNotValidValue(): void
    {
        $value = '1';
        $this->expectException(\TypeError::class);
        new class($value) extends IntValueObject {};
    }
}

我能想到的一个解释是,在测试期间,IntValueObject::__construct('1') 是从未使用 declare(strict_types=1); 的代码中调用的,因此字符串 '1' 被强制转换为整数 1。在这种情况下不会抛出 TypeError(但对于字符串 'foo' 会抛出 - 正如您在问题中描述的行为)。

原因

原因是使用了生成的模拟:

<?php declare (strict_types = 1);

...

        $this->expectException(\TypeError::class);
        $this->getMockForAbstractClass(IntValueObject::class, [$value]);

...

在这种情况下没有 TypeError 可能出乎你的意料,因为你有标量严格类型,但仍然看到构造函数的字符串 '1' 到整数 1 的类型强制第一个参数。

不匹配

然而 TypeError 只有当调用 IntValueObject::__construct(int $value) 的代码有 declare(strict_types=1).

虽然测试代码 declare(strict_types=1) 一定不能 是调用构造函数方法的代码 - 因为没有抛出 TypeError

真实

幕后 $this->getMockForAbstractClass(...); 使用 Instantiator from the Doctrine project which is making use of PHP reflection (meta-programming)。由于这些方法都是内部代码,declare(strict_types=1) 无效并且没有 TypeError 了。

与以下代码示例比较:

<?php declare(strict_types=1);

class Foo {
    public function __construct(int $integer) {
        $this->integer = $integer;
    }
}

try {
    $foo = new Foo('1');
} catch (TypeError $e) {
} finally {
    assert(isset($e), 'TypeError was thrown');
    assert(!isset($foo), '$foo is unset');
}

$foo = (new ReflectionClass(Foo::class))->newInstance('1');
var_dump($foo);

在启用断言的情况下执行时,输出如下:

object(Foo)#3 (1) {
  ["integer"]=>
  int(1)
}

在 try-block 中,TypeError 被抛出 new 正如你所期望的那样。

但之后用 PHP 反射实例化时就不是了。

(另见https://3v4l.org/aZTJl)

补救措施

添加一个 class 到你的测试套件, 真正模拟抽象基础 class 并将它放在它的测试旁边,这样你可以轻松使用它:

<?hpp declare(strict_types=1);

class IntValueObjectMock extends IntValueObject
{...}

然后在你的测试中使用IntValueObjectMock

        $value = '1';
        $this->expectException(\TypeError::class);
        new IntValueObjectMock($value);

或者直接扩展时使用 anonymous class

        $value = '1';
        $this->expectException(\TypeError::class);
        new class($value) extends IntValueObject {};

或者使用 PHP 反射或 run static code-analysis 对您自己的构造函数方法进行类型检查,其好处是无需实例化即可检测到此类问题 - 因此无需额外测试 -涉及代码。