PHP 和 PHPUnit:如何调用参数类型错误的方法(或函数)然后成功?

Mic*_*has 3 php phpunit type-hinting php-8.2

我想测试(使用 PHPUnit)一个包含 foreach 循环的方法。我想要完整的路径覆盖。原始代码有点太复杂,所以我在下面创建了一个最小的示例来说明我的问题。foreachPHP中的循环有3种情况。(我使用 PHP 8.2。)

\n
    \n
  1. 空可迭代的东西。
  2. \n
  3. 不是空的可迭代的东西。
  4. \n
  5. 不可迭代的东西。
  6. \n
\n
    public function dodo(array $a)\n    {\n        foreach ($a as $one) {\n            return $one;\n        }\n\n        return null;\n    }\n
Run Code Online (Sandbox Code Playgroud)\n

很容易涵盖前两个:非空数组和空数组。但是我如何传递一个不可迭代的东西作为函数参数呢?我尝试了几种方法,但我\xe2\x80\x99已经得到了类型错误。

\n
$something->dodo(null); # TypeError\n\ncall_user_func([$something, 'dodo'], null); # TypeError\n\n$rClass = new ReflectionClass($something);\n$rMethod = $rClass->getMethod('dodo');\n$rMethod->invoke($something, null); # TypeError\n
Run Code Online (Sandbox Code Playgroud)\n

我不想\xe2\x80\x99 不想从方法定义中删除或更改类型。这会使代码的可读性降低一些。有办法解决吗?如何编写一个涵盖 foreach 循环的所有情况的测试?

\n

换句话说:\n我如何使用dodo错误类型的参数调用?我想编写具有非常高的代码路径覆盖率的测试。

\n

Chr*_*oph 11

当您添加array为类型声明时,您基本上是告诉 PHP 确保不允许将其他类型传递给您的方法。PHP 确保没有其他类型被传递的方式是抛出TypeError你所看到的。

在某种程度上,您的代码相当于以下内容:

public function dodo($a)
{
    if (!is_array($a)) {
        throw new TypeError(...);
    }

    foreach ($a as $one) {
        return $one;
    }

    return null;
}
Run Code Online (Sandbox Code Playgroud)

你基本上回答了你自己的问题。您可以使用非数组参数调用该方法,就像您所做的那样:$something->dodo(null);。你TypeError看到的就是预期的结果。

由于 PHP 已经注意到您在这里只处理数组,因此实际上您已经涵盖了该foreach循环的所有情况。

现在这并没有回答为什么 PHPUnit 报告三种可能的路径的问题。我设置了您的示例来验证行为,实际上,它报告了三个路径,而我的测试只能覆盖其中两个路径。

PHPUnit 本身使用 Xdebug 来计算覆盖率,经过一番挖掘,我在 Xdebug bugtracker 中发现了这个问题,指出:

目前,ZEND_FE_RESET_R/RW 和 ZEND_FE_FETCH_R/RW 都被认为有两个输出分支,这会在生成代码覆盖率的同时添加一个额外的路径和一个额外的分支,而无需向用户提供任何合理的解释。

现在我绝对不是 PHP 操作码方面的专家,但这可能与您正在努力解决的一个未发现的分支密切相关。请参阅@bishop 的精彩回答,其中有更多详细信息。

在我个人看来,你不应该太担心未覆盖的路径,你为自己设置了一个非常高的清除标准,即使不是不可能的(正如你在这里所经历的那样)。我强烈推荐这篇文章,其中作者介绍了(双关语)不同的覆盖率指标。作者在文章中得出结论:

100% 路径覆盖无疑是圣杯,在合理可能的情况下,我认为即使您没有达到目标,这也是一个很好的指标。

所以总结一下,不要给自己太大的压力。正如他们所说,完美是善良的敌人。


bis*_*hop 5

TL;DR:可以捕获TypeError单元测试中的内容,但类型强制并不是 OP 中假设的问题。问题在于 PHP 引擎如何计算通过foreach.

\n
\n

回答所提出的问题:

\n
\n

如何传递不可迭代的东西作为函数参数?我尝试了几种方法,但我\xe2\x80\x99已经得到了类型错误。

\n
\n

鉴于此源代码:

\n
<?php declare(strict_types=1);\n\nnamespace App;\n\nclass Example\n{\n    public function dodo(array $a)\n    {\n        foreach ($a as $one) {\n            return $one;\n        }\n\n        return null;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

那么这个测试用例实现了所请求的三个场景:

\n
<?php declare(strict_types=1);                                                                                                       \n                                                                                                                                     \nuse PHPUnit\\Framework\\TestCase;                                                                                                      \n                                                                                                                                     \nfinal class ExampleTest extends TestCase                                                                                             \n{                                                                                                                                    \n    public function testReturnsNullOfIterableWithoutFirstElement(): void                                                             \n    {                                                                                                                                \n        $example = new App\\Example;                                                                                                  \n                                                                                                                                     \n        $result = $example->dodo([]);                                                                                                \n                                                                                                                                     \n        $this->assertNull($result);                                                                                                  \n    }                                                                                                                                \n                                                                                                                                     \n    public function testReturnsFirstElementOfIterableWithElements(): void                                                            \n    {                                                                                                                                \n        $example = new App\\Example;                                                                                                  \n                                                                                                                                     \n        $result = $example->dodo([ \'a\' ]);                                                                                           \n                                                                                                                                     \n        $this->assertSame(\'a\', $result);                                                                                             \n    }                                                                                                                                \n                                                                                                                                     \n    public function testThrowTypeErrorOnNonIterable(): void                                                                          \n    {                                                                                                                                \n        $this->expectException(\\TypeError::class);                                                                                   \n                                                                                                                                     \n        $example = new App\\Example;                                                                                                  \n                                                                                                                                     \n        $result = $example->dodo(null);                                                                                               \n    }                                                                                                                                \n}                                                                                                                                    \n
Run Code Online (Sandbox Code Playgroud)\n

线路和分支机构的覆盖范围证明了这一点:

\n
# ./vendor/bin/phpunit  --coverage-text\nPHPUnit 10.2.6 by Sebastian Bergmann and contributors.\n\nRuntime:       PHP 8.2.8 with Xdebug 3.2.2\nConfiguration: /app/phpunit.xml\n\n...                                                                 3 / 3 (100%)\n\nTime: 00:00.103, Memory: 12.00 MB\n\nOK (3 tests, 3 assertions)\n\n\nCode Coverage Report:   \n  2023-07-18 00:09:26   \n                        \n Summary:               \n  Classes: 100.00% (1/1)\n  Methods: 100.00% (1/1)\n  Lines:   100.00% (3/3)\n\nApp\\Example\n  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  3/  3)\n
Run Code Online (Sandbox Code Playgroud)\n
\n

但是,正如评论中所述,使用 Xdebug 路径覆盖率分析功能时会出现问题:

\n
$ ./vendor/bin/phpunit --coverage-text\nPHPUnit 10.2.6 by Sebastian Bergmann and contributors.\n\nRuntime:       PHP 8.2.8 with Xdebug 3.2.2\nConfiguration: /app/phpunit.xml\n\n...                                                                 3 / 3 (100%)\n\nTime: 00:00.529, Memory: 183.48 MB\n\nOK (3 tests, 3 assertions)\n\n\nCode Coverage Report:   \n  2023-07-18 18:52:13   \n                        \n Summary:               \n  Classes: 100.00% (1/1)\n  Methods: 100.00% (1/1)\n  Paths:   66.67% (2/3)    <<<<---- WHY????\n  Branches:   100.00% (4/4)\n  Lines:   100.00% (3/3)\n\nApp\\Example\n  Methods: 100.00% ( 1/ 1)   Paths:  66.67% (  2/  3)   Branches: 100.00% (  4/  4)   Lines: 100.00% (  3/  3)\n
Run Code Online (Sandbox Code Playgroud)\n

所以,隐含的问题是:为什么这条路径没有被覆盖?OP推测第三条路径是在给出“不可迭代”时,但事实并非如此。

\n

使用3v4l.org 和 Vulcan Logic Dumper深入研究路径分析,请注意foreach(on line 9) 的内部行为及其重置和获取操作生成两个输出分支 (column O):

\n
line      #* E I O op                           fetch          ext  return  operands\n-------------------------------------------------------------------------------------\n\n    9     1      > FE_RESET_R                                       $2      !0, ->6\n          2    > > FE_FETCH_R                                               $2, !1, ->6\n
Run Code Online (Sandbox Code Playgroud)\n

为什么?因为进入循环后,引擎必须决定变量是否可迭代(这是一条路径),如果是,则开始迭代(第二条路径)。就好像每个foreach在检查可迭代性之前都有一个隐式的“if”。

\n

提供路径覆盖数据的 Xdebug 忠实地报告了这一点。这也是为什么,正如在另一个答案的聊天中提到的,删除类型提示并强制无效类型满足路径分析。

\n

不幸的是,无论如何,没有办法消除这种引擎行为;这就是foreach工作原理。

\n

一个功能请求要探索是否可以在 Xdebug 3.3 中掩盖这一点,但目前使用严格类型绕过它的唯一方法是不使用foreach

\n
# ./vendor/bin/phpunit  --coverage-text\nPHPUnit 10.2.6 by Sebastian Bergmann and contributors.\n\nRuntime:       PHP 8.2.8 with Xdebug 3.2.2\nConfiguration: /app/phpunit.xml\n\n...                                                                 3 / 3 (100%)\n\nTime: 00:00.103, Memory: 12.00 MB\n\nOK (3 tests, 3 assertions)\n\n\nCode Coverage Report:   \n  2023-07-18 00:09:26   \n                        \n Summary:               \n  Classes: 100.00% (1/1)\n  Methods: 100.00% (1/1)\n  Lines:   100.00% (3/3)\n\nApp\\Example\n  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  3/  3)\n
Run Code Online (Sandbox Code Playgroud)\n

产生:

\n
$ ./vendor/bin/phpunit --coverage-text\nPHPUnit 10.2.6 by Sebastian Bergmann and contributors.\n\nRuntime:       PHP 8.2.8 with Xdebug 3.2.2\nConfiguration: /app/phpunit.xml\n\n...                                                                 3 / 3 (100%)\n\nTime: 00:00.528, Memory: 183.48 MB\n\nOK (3 tests, 3 assertions)\n\n\nCode Coverage Report:   \n  2023-07-18 19:30:16   \n                        \n Summary:               \n  Classes: 100.00% (1/1)\n  Methods: 100.00% (1/1)\n  Paths:   100.00% (2/2)\n  Branches:   100.00% (3/3)\n  Lines:   100.00% (3/3)\n\nApp\\Example\n  Methods: 100.00% ( 1/ 1)   Paths: 100.00% (  2/  2)   Branches: 100.00% (  3/  3)   Lines: 100.00% (  3/  3)\n
Run Code Online (Sandbox Code Playgroud)\n

我怀疑这对于OP来说通常不是一个令人满意的改变。因此,在代码覆盖率更好地支持 之前foreach,我建议放宽对路径覆盖率指标的要求。

\n