替换 PHPUnit 方法 `withConsecutive` (在 PHPUnit 10 中已弃用)

24
由于 PHPUnit 10 中将删除方法 withConsecutive(在 9.6 版本中已被弃用),因此我需要将所有出现该方法的地方替换为新代码。
尝试寻找解决方案,但没有找到任何合理的解决方案。
例如,我有一段代码:
    $this->personServiceMock->expects($this->exactly(2))
        ->method('prepare')
        ->withConsecutive(
            [$personFirst, $employeeFirst],
            [$personSecond, $employeeSecond],
        )
        ->willReturnOnConsecutiveCalls($personDTO, $personSecondDTO);

我应该用哪个代码替换withConsecutive
附言:官方网站上的文档仍然显示如何使用withConsecutive

5
是的,废弃并删除方法而不提供替代方案令人失望 :( 这里有一个讨论:https://github.com/sebastianbergmann/phpunit/issues/4026,我没有看到任何好的理由来删除它。 - Roman Kliuchko
4
@RomanKliuchko 我也没有看到什么好的论据。不幸的是,Sebastian似乎总是在不考虑PHPUnit用户的情况下删除接口。他给出的删除我经常使用的另一种方法的原因是他认为很少有人使用它,这令人难以置信。 - rafark
1
伯格曼众所周知是个无知的人。他甚至选择随机的政治观点将它们印在用户终端上,滥用了很多人依赖他的免费图书馆这一事实。 - undefined
12个回答

22

我已经用以下内容替换了withConsecutive。

$matcher = $this->exactly(2);
$this->service
    ->expects($matcher)
    ->method('functionName')
    ->willReturnCallback(function (string $key, string $value) use ($matcher,$expected1, $expected2) {
        match ($matcher->numberOfInvocations()) {
            1 =>  $this->assertEquals($expected1, $value),
            2 =>  $this->assertEquals($expected2, $value),
        };
    });

这个解决方案真的很有效!非常感谢你的分享。 - Tomas Votruba
我已经使用你的答案创建了一个Rector规则来自动化这个升级:https://github.com/rectorphp/rector-phpunit/pull/246谢谢你。 - Tomas Votruba
1
谢谢你的赞赏,Tomas。很高兴听到它对你在Rector方面有所帮助。我真的是Rector Package的粉丝,并且已经在我的项目中使用过它 :) - Awais Mushtaq
@TomasVotruba 在提交中提及Awais作为作者可能是有意义的,不是吗? - undefined
@OddDev 我已经在 PR https://github.com/rectorphp/rector-phpunit/pull/246 中正确地添加了链接,希望这样就足够了 :) - undefined
@AwaisMushtaq 很高兴听到这个消息,一切都圆满了:),如果你在Github上需要关于Rector的任何帮助或者有规则建议,请告诉我。我很乐意帮忙。 - undefined

11

我刚刚升级到PHPUnit 10并遇到了同样的问题。这是我想出的解决方案:

$this->personServiceMock
    ->method('prepare')
    ->willReturnCallback(fn($person, $employee) =>
        match([$person, $employee]) {
            [$personFirst, $employeeFirst] => $personDTO,
            [$personSecond, $employeeSecond] => $personSecondDTO
        }
    );

如果在match块中传递了与预期不同的内容,则PHP会抛出UnhandledMatchError
编辑:一些评论指出了此处无法知道函数被调用次数的限制。这有点像一个hack,但我们可以像这样手动计算函数调用次数:
// Keep reference of the arguments passed in an array:
$callParams = [];

$this->personServiceMock
    ->method('prepare')
// Pass the callParams array by reference:
    ->willReturnCallback(function($person, $employee)use(&$callParams) {
// Store the current arguments in the array:
        array_push($callParams, func_get_args());

        match([$person, $employee]) {
            [$personFirst, $employeeFirst] => $personDTO,
            [$personSecond, $employeeSecond] => $personSecondDTO
        }
    });

// Check that an expected argument call is present in the $callParams array:
self::assertContains(["Person1",  "Employee1"], $callParams);

2
这很好,但是你的解决方案没有考虑方法运行的顺序。 - yAnTar
我也曾考虑使用这个方法作为替代,但据我所知,匹配函数更像是一个开关函数(https://www.php.net/manual/de/control-structures.match.php),只检查给定参数($person,$employee)是否与match函数中描述的条件之一匹配(例如[$personFirst,$employeeFirst])。尽管如此,您将不知道函数是否使用了所有描述的条件。因此,如果准备方法主要使用[$personFirst,$employeeFirst]而从未使用过[$personSecond,$employeeSecond],则不会出现错误。 - flumingo

3

对我而言,以下方法有效:

$expected = ['value1', 'value2'];
$matcher = $this->exactly(count($expected));
$this->mockedObject->expects($matcher)->method('test')->with(
   $this->callback(function($param) use ($expected) {
        $this->assertEquals($param, $expected[$matcher->getInvocationCount() - 1]);
   return true;
   })
)

对于两个大对象,我们有一个简单的消息:“对象不相等”,没有差异,也没有任何信息。 - ZhukV

2

我遇到了同样的问题,虽然我认为这不是世界上最实用的解决方案,但你可以尝试一下。

你需要一个简单的辅助函数。

public function consecutiveCalls(...$args): callable
{
    $count = 0;
    return function ($arg) use (&$count, $args) {
        return $arg === $args[$count++];
    };
}

接下来,我们将用with替代已弃用的withConsecutive,并为每个参数添加回调函数,以返回带有连续参数的辅助函数。

$this->personServiceMock->expects($this->exactly(2))
    ->method('prepare')
    ->with(
        self::callback(self::consecutiveCalls($personFirst, $personSecond)),
        self::callback(self::consecutiveCalls($employeeFirst, $employeeSecond)),
    )
    ->willReturnOnConsecutiveCalls($personDTO, $personSecondDTO);

1
我认为,willReturnMap也可以是一个有用的替代方案。
$mock = $this->createMock(MyClass::class):

$mock->expects(self::exactly(3))
     ->method('get')
     ->willReturnMap([
         [1, 'Foo'],
         [9, 'Bar'],
         [5, 'Baz'],
     ]);

self::assertSame('Bar', $mock->get(9));
self::assertSame('Baz', $mock->get(5));
self::assertSame('Foo', $mock->get(1));


请注意,调用顺序不会由您传递的映射来定义。
所以,如果调用顺序对您来说不重要,我认为这是最不会产生干扰的解决方案。

对我来说,调用的顺序非常重要。但还是谢谢你的解决方案。 - undefined
那样的话,你就围绕着调用计数器来操作。但是我要指出的是,期望调用的顺序并不是一个很好的方法。 - undefined

0
看起来似乎没有现成的解决方案。 所以,我找到了几个解决方案:
  1. 使用你自己的特性,该特性实现了withConsecutive方法。
  2. 使用prophecy或mockery进行模拟。

使用Mockery有什么替代方案? - rafark
喜欢这个特性。我已经实现了自己的版本,并且想要分享,但是链接中的解决方案也可以使用。 - mvmoay

0
另一种方法是使用匿名函数和引用传递的数组来捕获传递给函数的参数,然后在之后进行断言。
考虑以下示例:
$persistedProducts = [];
$entityManager->expects(self::exactly(2))
    ->method('persist')
    ->with(
        self::callback(
            static function (ProductInterface $product) use (&$persistedProducts) {
                $persistedProducts[] = $product;

                return true;
            }
        )
    );


// Execute the code under test here
// e.g. new ProductUpdater($em)->update();


// First persisted product is the one that was returned from mock repository,
// so we can compare identically.
self::assertSame($product123, $persistedProducts[0]);

// Second persisted product is a new product, we need to check its attributes
self::assertSame(456, $persistedProducts[1]->getId());
self::assertSame(73, $persistedProducts[1]->getSold());

在这种情况下,由于`persist()`返回`void`,我选择不使用`willReturnCallback`。虽然在这个上下文中可以应用相同的方法,但从语义上讲并不理想。这是因为在这个实例中,你的主要关注点不是返回值,尽管在后续使用`with`或`assert`语句时可能会关注返回值。

0
我已经为传递给PHPUnit方法->willReturnCallback()的回调函数创建了一个工厂,它的实现如下(灵感来自:@Awais Mushtaq):
protected function getReturnCallbackFn(
    InvocationOrder $matcher,
    array $paramConsuitiveCalls,
    array $returnConsuitiveCalls
): \Closure
{
    if (!empty($returnConsuitiveCalls) && count($paramConsuitiveCalls) !== count($returnConsuitiveCalls)) {
        throw new \InvalidArgumentException('Count of params and return values mismatch.');
    }
    return function (...$args) use (
        $matcher,
        $paramConsuitiveCalls,
        $returnConsuitiveCalls
    ) {
        $i = $matcher->numberOfInvocations() - 1;
        if (!array_key_exists($i, $paramConsuitiveCalls)) {
            throw new \OutOfRangeException(sprintf(
                'Iterations expected [%d] against current [%d] executed.',
                count($returnConsuitiveCalls),
                $matcher->numberOfInvocations()),
            );
        }
        if (empty($args)) {
            $this->assertEquals($paramConsuitiveCalls[$i], []);
        } else {
            foreach ($args as $argI => $arg) {
                $this->assertEquals($paramConsuitiveCalls[$i][$argI], $arg);
            }
        }
        if (empty($returnConsuitiveCalls)) {
            return;
        }
        return $returnConsuitiveCalls[$i];
    };
}

以及用法:

$params = [[123], [234]];
$ret = [$sampleData1Call, $sampleData2Call];
$matcher = $this->exactly(count($params));
$stub
    ->method('getById')
    ->willReturnCallback($this->getReturnCallbackFn($matcher, $params, $ret))
;

0
另一个解决方案:
$mock->method('run')->with($this->callback(function (string $arg) {
            static $i = 0;
            [
                1 => function () use ($arg) {$this->assertEquals('this', $arg);},
                2 => function () use ($arg) {$this->assertEquals('that', $arg);},
            ][++$i]();
            return true;
        }))

另外:
    $mock->method('run')->with($this->callback(function (string $arg) {
        $inputs = ['this', 'that'];
        static $i = -1;
        $this->assertEquals($inputs[--$i], $arg);
        return true;
    }));

-1
我们有一个庞大的代码库,经常使用withConsecutive。为了避免需要修复每个测试,我们创建了一个phpunit-extensions包来简化过渡。
这种表示法应该相当容易找到和替换现有的用法:
$mock->method('myMethod')->withConsecutive([123, 'foobar'], [456]); 变成:
$mock->method('myMethod')->with(...\DR\PHPUnitExtensions\Mock\consecutive([123, 'foobar'], [456])); 如果使用PHPStorm的结构搜索和替换功能,甚至更加简单: https://www.jetbrains.com/help/phpstorm/structural-search-and-replace.html

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接