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

huangapple go评论59阅读模式
英文:

Replace PHPUnit method `withConsecutive` (abandoned in PHPUnit 10)

问题

方法withConsecutive将在PHPUnit 10中被删除(在9.6中已弃用),我需要将所有出现该方法的地方替换为新代码。

尝试寻找一些解决方案,但没有找到任何合理的解决方案。

例如,我有以下代码:

$this->personServiceMock->expects($this->exactly(2))
    ->method('prepare')
    ->withConsecutive(
        [$personFirst, $employeeFirst],
        [$personSecond, $employeeSecond],
    )
    ->willReturnOnConsecutiveCalls($personDTO, $personSecondDTO);

我应该用什么代码替换withConsecutive

附注:官方网站上的文档仍然显示如何使用withConsecutive

英文:

As method withConsecutive will be deleted in PHPUnit 10 (in 9.6 it's deprecated) I need to replace all of occurrences of this method to new code.

Try to find some solutions and didn't find any of reasonable solution.

For example, I have a code

    $this->personServiceMock->expects($this->exactly(2))
        ->method('prepare')
        ->withConsecutive(
            [$personFirst, $employeeFirst],
            [$personSecond, $employeeSecond],
        )
        ->willReturnOnConsecutiveCalls($personDTO, $personSecondDTO);

To which code should I replace withConsecutive ?

P.S. Documentation on official site still shows how use withConsecutive

答案1

得分: 19

我已经用以下代码替换了"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),
        };
    });

如果您需要任何进一步的帮助,请随时告诉我。

英文:

I have replaced withConsecutive with the following.

$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),
        };
    });

答案2

得分: 11

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

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

如果在match块中传递的模拟方法与预期不符,PHP会抛出UnhandledMatchError

编辑:一些评论指出了这里的限制,即不知道函数被调用了多少次。这有点取巧,但我们可以手动计算函数调用的次数,如下所示:

// 保留传递的参数的引用数组:
$callParams = [];

$this->personServiceMock
    ->method('prepare')
    // 通过引用传递callParams数组:
    ->willReturnCallback(function($person, $employee) use (&$callParams) {
        // 将当前参数存储在数组中:
        array_push($callParams, func_get_args());

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

// 检查$callParams数组中是否存在预期的参数调用:
self::assertContains(["Person1", "Employee1"], $callParams);
英文:

I've just upgraded to PHPUnit 10 and faced the same issue. Here's the solution I came to:

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

If the mocked method is passed something other than what's expected in the match block, PHP will throw a UnhandledMatchError.

Edit: Some comments have pointed out the limitation here of not knowing how many times the function has been called. This is a bit of a hack, but we could count the function calls manually like this:

// 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);

答案3

得分: 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;
    })
)
英文:

For me the following worked:

$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;
   })
)

答案4

得分: 2

以下是您要翻译的内容:

I ran into the same issue and although I don't think this is the most practical solution in the world, you can try it.

You will need a simple helper function.

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

Then we'll replace deprecated withConsecutive with with and for every parameter we'll add a callback that will return our helper function with consecutive parameters.

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

I ran into the same issue and although I don't think this is the most practical solution in the world, you can try it.

You will need a simple helper function.

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

Then we'll replace deprecated withConsecutive with with and for every parameter we'll add callback that will return our helper function with consecutive parameters.

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

答案5

得分: 0

看起来没有现成的解决方案。所以,我找到了几种解决方法:

  1. 使用自定义特性,该特性实现了withConsecutive方法。
  2. 使用Prophecy或Mockery进行模拟。
英文:

Looks like there are not exists solution from the box.
So, what I found - several solutions

  1. Use your own trait which implements method withConsecutive
  2. Use prophecy or mockery for mocking.

答案6

得分: 0

我已经创建了一个工厂,用于传递给->willReturnCallback() PHPUnit方法的回调,代码如下(灵感来源:@Awais Mushtaq):

protected function getReturnCallbackFn(
    InvocationOrder $matcher,
    array $paramConsuitiveCalls,
    array $returnConsuitiveCalls
): \Closure
{
    if (!empty($returnConsuitiveCalls) && count($paramConsuitiveCalls) !== count($returnConsuitiveCalls)) {
        throw new \InvalidArgumentException('参数和返回值的数量不匹配。');
    }
    return function (...$args) use (
        $matcher,
        $paramConsuitiveCalls,
        $returnConsuitiveCalls
    ) {
        $i = $matcher->numberOfInvocations() - 1;
        if (!array_key_exists($i, $paramConsuitiveCalls)) {
            throw new \OutOfRangeException(sprintf(
                '期望的迭代次数 [%d] 与当前执行的次数 [%d] 不匹配。',
                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));

请注意,这是PHP代码的翻译。

英文:

I have created the factory for a callback passed to ->willReturnCallback() PHPUnit method and it goes like this (inspiration: @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];
    };
}

And usage:

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

答案7

得分: -1

我们有一个庞大的代码库,经常使用 withConsecutive。为了避免不得不修复每个测试,我们创建了一个 phpunit-extensions 包来简化过渡。

这种记法应该很容易找到并替换现有的用法:<br>
$mock-&gt;method(&#39;myMethod&#39;)-&gt;withConsecutive([123, &#39;foobar&#39;], [456]);

变成:<br>
$mock-&gt;method(&#39;myMethod&#39;)-&gt;with(...\DR\PHPUnitExtensions\Mock\consecutive([123, &#39;foobar&#39;], [456]));

使用PHPStorm的结构搜索和替换会更容易:
https://www.jetbrains.com/help/phpstorm/structural-search-and-replace.html

英文:

We have a large codebase and used withConsecutive frequently. To avoid having to fix every test we created a phpunit-extensions package to ease the transition.

The notation should be fairly easy to find and replace existing usages:<br>
$mock-&gt;method(&#39;myMethod&#39;)-&gt;withConsecutive([123, &#39;foobar&#39;], [456]);

To:<br>
$mock-&gt;method(&#39;myMethod&#39;)-&gt;with(...\DR\PHPUnitExtensions\Mock\consecutive([123, &#39;foobar&#39;], [456]));

It's even easier with PHPStorm's structural search and replace:
https://www.jetbrains.com/help/phpstorm/structural-search-and-replace.html

答案8

得分: -1

以下是您要翻译的内容:

"For our applications we use our custom constraint with specific map.
Previously we try to use callback (with call assertEquals inside), but callback must return only boolean and if we try to check objects, the message was be simple - objects are not equals, without diff and without any information about problem.

As result, we create our constraint:

<?php

declare(strict_types = 1);

namespace Acme\Tests\PhpUnit\Framework\Constraint;

use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
use SebastianBergmann\Comparator\ComparisonFailure;
use SebastianBergmann\Comparator\Factory;

class ConsecutiveMatches extends Constraint
{
    /**
     * @var ComparisonFailure|null
     */
    private ?ComparisonFailure $failure = null;

    /**
     * Constructor.
     *
     * @param InvocationOrder   $invocation
     * @param array<int, mixed> $map
     * @param bool              $strict
     */
    public function __construct(
        private readonly InvocationOrder $invocation,
        private readonly array           $map,
        private readonly bool            $strict = true,
    ) {
    }

    /**
     * {@inheritdoc}
     */
    protected function matches(mixed $other): bool
    {
        $invokedCount = $this->invocation->numberOfInvocations();

        if (array_key_exists($invokedCount - 1, $this->map)) {
            $expectedParam = $this->map[$invokedCount - 1];
        } else if ($this->strict) {
            throw new \InvalidArgumentException(sprintf(
                'Missed argument for matches (%d times).',
                $invokedCount
            ));
        }

        $comparator = Factory::getInstance()->getComparatorFor($expectedParam, $other);

        try {
            $comparator->assertEquals($expectedParam, $other);
        } catch (ComparisonFailure $error) {
            $this->failure = $error;

            return false;
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function failureDescription(mixed $other): string
    {
        return $this->failure ? $this->failure->getDiff() : parent::failureDescription($other);
    }

    /**
     * {@inheritdoc}
     */
    public function toString(): string
    {
        return '';
    }
}

In this constraint we get the argument from map by invocation number.

And it very easy to usage:

#[Test]
public function shouldFoo(): void
{
    $mock = $this->createMock(MyClass::class);

    $matcher = new InvokedCount(2); // Should call 2 times

    $mock->expects($matcher)
        ->method('someMethod')
        ->with(new ConsecutiveMatches($matcher, [$expectedArgumentForFirstCall, $expectedArgumentForSecondCall]))
        ->willReturnCallback(function () {
            // You logic for return value.
            // You can use custom map too for returns.
        });
}

As result, we can use our constraint in more places."

英文:

For our applications we use our custom constraint with specific map.
Previously we try to use callback (with call assertEquals inside), but callback must return only boolean and if we try to check objects, the message was be simple - objects are not equals, without diff and without any information about problem.

As result, we create our constaint:

&lt;?php

declare(strict_types = 1);

namespace Acme\Tests\PhpUnit\Framework\Constraint;

use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
use SebastianBergmann\Comparator\ComparisonFailure;
use SebastianBergmann\Comparator\Factory;

class ConsecutiveMatches extends Constraint
{
    /**
     * @var ComparisonFailure|null
     */
    private ?ComparisonFailure $failure = null;

    /**
     * Constructor.
     *
     * @param InvocationOrder   $invocation
     * @param array&lt;int, mixed&gt; $map
     * @param bool              $strict
     */
    public function __construct(
        private readonly InvocationOrder $invocation,
        private readonly array           $map,
        private readonly bool            $strict = true,
    ) {
    }

    /**
     * {@inheritdoc}
     */
    protected function matches(mixed $other): bool
    {
        $invokedCount = $this-&gt;invocation-&gt;numberOfInvocations();

        if (\array_key_exists($invokedCount - 1, $this-&gt;map)) {
            $expectedParam = $this-&gt;map[$invokedCount - 1];
        } else if ($this-&gt;strict) {
            throw new \InvalidArgumentException(\sprintf(
                &#39;Missed argument for matches (%d times).&#39;,
                $invokedCount
            ));
        }

        $comparator = Factory::getInstance()-&gt;getComparatorFor($expectedParam, $other);

        try {
            $comparator-&gt;assertEquals($expectedParam, $other);
        } catch (ComparisonFailure $error) {
            $this-&gt;failure = $error;

            return false;
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function failureDescription(mixed $other): string
    {
        return $this-&gt;failure ? $this-&gt;failure-&gt;getDiff() : parent::failureDescription($other);
    }

    /**
     * {@inheritdoc}
     */
    public function toString(): string
    {
        return &#39;&#39;;
    }
}

In this constraint we get the argument from map by invocation number.

And it very easy to usage:

#[Test]
public function shouldFoo(): void
{
    $mock = $this-&gt;createMock(MyClass::class);

    $matcher = new InvokedCount(2); // Should call 2 times

    $mock-&gt;expects($matcher)
        -&gt;method(&#39;someMethod&#39;)
        -&gt;with(new ConsecutiveMatches($matcher, [$expectedArgumentForFirstCall, $expectedArgumentForSecondCall]))
        -&gt;willReturnCallback(function () {
            // You logic for return value.
            // You can use custom map too for returns.
        });
}

As result, we can use our constraint in more places.

答案9

得分: -2

$params = ['foo', 'bar',];
$mockObject->expects($this->exactly(2))
->method('call')
->willReturnCallback(function (string $param) use (&$params) {
$this::assertSame(array_shift($params), $param);
})
->willReturnOnConsecutiveCalls('foo_result', 'bar_result');

或者可以从willReturnCallback 返回结果,而不是使用 willReturnOnConsecutiveCalls

英文:
$params = [&#39;foo&#39;, &#39;bar&#39;,];
$mockObject-&gt;expects($this-&gt;exactly(2))
-&gt;method(&#39;call&#39;)
-&gt;willReturnCallback(function (string $param) use (&amp;$params) {
$this::assertSame(\array_shift($params), $param);
})
-&gt;willReturnOnConsecutiveCalls(&#39;foo_result&#39;, &#39;bar_result&#39;);

Or instead of using willReturnOnConsecutiveCalls you can return result from willReturnCallback

huangapple
  • 本文由 发表于 2023年2月9日 00:44:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/75389000.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定