Search code examples
phpobjectphpunit

PHPUnit withConsecutive strange behaviour


Consider the next test:

class User
{
}

class UserRepository
{
    public function getByName($name)
    {
    }

    public function getByUser(User $user)
    {
    }
}

class UserController
{
    private $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function findByNames($name1, $name2)
    {
        $this->userRepository->getByName($name1);
        $this->userRepository->getByName($name2);
    }

    public function findByUsers($user1, $user2)
    {
        $this->userRepository->getByUser($user1);
        $this->userRepository->getByUser($user2);
    }
}

class WithConsecutiveTest extends \PHPUnit_Framework_TestCase
{
    /**
     * This test is fails if some of "Alice" or "Bob" string changed. This is expected behaviour.
     */
    public function testWithConsecutiveOnStrings()
    {
        $name1 = 'Alice';
        $name2 = 'Bob';

        $userRepository = $this->createMock(UserRepository::class);

        $userRepository
            ->expects($this->exactly(2))
            ->method('getByName')
            ->withConsecutive(
                [$name1], // change to $name2 and test fails
                [$name2]
            );

        $userController = new UserController($userRepository);
        $userController->findByNames($name1, $name2);
    }

    /**
     * This test is NOT fails if in "withConsecutive()" call $user1 changed to $user2. This is unexpected behaviour.
     */
    public function testWithConsecutiveOnObjects()
    {
        $user1 = $this->createMock(User::class);
        $user2 = $this->createMock(User::class);

        $this->assertEquals($user1, $user2);
        $this->assertNotSame($user1, $user2);

        $userRepository = $this->createMock(UserRepository::class);

        $userRepository
            ->expects($this->exactly(2))
            ->method('getByUser')
            ->withConsecutive(
                [$user1], // change to $user2 and test is also passed
                [$user2]
            );

        $userController = new UserController($userRepository);
        $userController->findByUsers($user1, $user2);
    }
}

First test with string arguments for "withConsecutive()" is works correctly, but second test with objects do some magic: something similar to weak comparison of two objects is here, so second test is passed in any case.

I tried with "[$this->callback(function($user1arg) use ($user1)){return $user1arg === $user1}]" constraint - this works well, but write a lot of this constraint is a some kind of monkey job.

Maybe it may have a more simple solution, instead of writing a lot of constraints with $this->callback(...) for objects?


Solution

  • Not everything in PHPUnit is perfect, but this should work for You. Of course property 'name' is only an example. If user does not holds any behaviour, You don't need to mock it.

    class User
    {
        private $name;
    
        public function setName($name)
        {
            $this->name = $name;
        }
    
        public function getName($name)
        {
            return $this->name;
        }
    }
    

    test method

    public function testWithConsecutiveOnObjects()
    {
        $user1 = new User();
        $user1->setName('Alice');
        $user2 = new User();
        $user2->setName('Bob');
        $this->assertNotEquals($user1, $user2);
        $this->assertNotSame($user1, $user2);
        $userRepository = $this->createMock(UserRepository::class);
        $userRepository
            ->expects($this->exactly(2))
            ->method('getByUser')
            ->withConsecutive(
                [$user1], //we are comparing values here, not references
                [$user2]
            );
        $userController = new UserController($userRepository);
        $userController->findByUsers($user1, $user2);
    }