19

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

yAnTar
  • 4,269
  • 9
  • 47
  • 73
  • 5
    Yeh, deprecating and removing the method without providing an alternative is disappointing :( Here's a discussion about it: https://github.com/sebastianbergmann/phpunit/issues/4026 and I don't see any good arguments for removing it. – Roman Kliuchko Feb 22 '23 at 14:43
  • 3
    @RomanKliuchko I don't see good arguments either. Unfortunately, Sebastian seems to remove interfaces all the time without considering the users of PHPUnit. The reason he gave for removing another method I extensively use was that he "didn't think" many people used it, unbelievable. – rafark Mar 25 '23 at 05:40

8 Answers8

13

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),
        };
    });
Awais Mushtaq
  • 633
  • 1
  • 10
  • 23
9

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);
Greg
  • 21,235
  • 17
  • 84
  • 107
  • 2
    This is good, but your solution doesn't count the order of running methods. – yAnTar Feb 17 '23 at 08:01
  • I was going to use this method as a replacement as well, but afaik the match function works more like a switch function (https://www.php.net/manual/de/control-structures.match.php) and only checks if the given parameter ($person, $employee) matches one of the conditions described in the match function (like [$personFirst, $employeeFirst]). Nevertheless you will not know if the function was called with all the conditions described. So you will not get an error if the prepare method is called mainly with [$personFirst, $employeeFirst] but never with [$personSecond, $employeeSecond]. – flumingo Mar 06 '23 at 09:37
3

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;
   })
)
  • For two large objects - we have simple message: `objects are not equals`, without diff and without any info. – ZhukV Aug 18 '23 at 11:53
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 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);
0

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.
mvmoay
  • 1,535
  • 3
  • 15
  • 26
yAnTar
  • 4,269
  • 9
  • 47
  • 73
-1

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:
$mock->method('myMethod')->withConsecutive([123, 'foobar'], [456]);

To:
$mock->method('myMethod')->with(...\DR\PHPUnitExtensions\Mock\consecutive([123, 'foobar'], [456]));

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

Frank Dekker
  • 29
  • 1
  • 4
-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 constaint:

<?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.

ZhukV
  • 2,892
  • 6
  • 25
  • 36
-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');

Or instead of using willReturnOnConsecutiveCalls you can return result from willReturnCallback