2
votes

Symfony 2.8.13 / Doctrine ORM 2.5.5 / PHPUnit 5.7.5

I want to test a method of a class that makes use of the doctrine entity manager. This public method calls a private one that instantiates a Bookmark entity, flushes it and returns this entity. Then later, in the tested method I need to access the entity Id. Everything is mocked excepted the Bookmark entity itself. The main problem is that there is no setId() method in my entity. Here is the code and my main idea to solve this issue but I don't know if it is correct ?

Tested class and method

class BookmarkManager
{
    //...

    public function __construct(TokenStorageInterface $tokenStorage, ObjectManager $em, Session $session)
    {
        //...
    }

    public function manage($bookmarkAction, $bookmarkId, $bookmarkEntity, $bookmarkEntityId)
    {
        //...
        $bookmark = $this->add($bookmarkEntity, $bookmarkEntityId);
        //...
        $bookmarkId = $bookmark->getId();
        //...
    }

    private function add($entity, $entityId)
    {
        //...
        $bookmark = new Bookmark();
        //...
        $this->em->persist($bookmark);
        $this->em->flush();

        return $bookmark;
    }
}

Test

class BookmarkManagerTest extends \PHPUnit_Framework_TestCase
{
    public function testThatRestaurantAdditionToBookmarksIsWellManaged()
    {
        //...
        //  THIS WON'T WORK AS NO setId() METHOD EXISTS
        $entityManagerMock->expects($this->once())
            ->method('persist')
            ->will($this->returnCallback(function ($bookmark) {
                if ($bookmark instanceof Bookmark) {
                    $bookmark->setId(1);
                }
            }));
        //...
        $bookManager = new BookmarkManager($tokenStorageMock, $entityManagerMock, $sessionMock);
        //...
    }
}

Solutions ?

1- Make usage of reflection class as proposed here :

$entityManagerMock->expects($this->once())
    ->method('persist')
    ->will($this->returnCallback(function ($bookmark) {
        if ($bookmark instanceof Bookmark) {
            $class = new \ReflectionClass($bookmark);
            $property = $class->getProperty('id');
            $property->setAccessible(true);
            $property->setValue($bookmark, 1);
            //$bookmark->setId(1);
        }
    }));

2- Create a test Boookmark entity that extends from the real one and add a setId() method. Then create a mock of this class and replace and customize the one got from the ReturnCallback method with this one ? It seems crappy...

Any thoughts ? Thanks for your help.

1
I know reflection is supposed to be evil, but I think the reflection method in you mention is the best option. Something feels wrong about creating classes just for testing, such as a TestBookmark or MockEntityManager. If you're going to be doing this regularly in tests I found creating a ReflectionSetterTrait that just has a method setProperty($object, $property, $value) to be useful.mickadoo
@mickadoo I agree, the trait is a good option. For entity manager I don't create a specific class, I just mock it using PHPUnit mock builder. Anyway, the reflection seems to be the solution in this case.Cruz

1 Answers

2
votes

The reflection looks interesting but it decreases readability of tests (mixing with mocks makes the situation tough).

I would create a fake for entity manager and implements there setting id based on reflection:

class MyEntityManager implements ObjectManager
{
    private $primaryIdForPersitingObject;

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

    ...

    public function persist($object)
    {
        $reflectionClass = new ReflectionClass(get_class($object));
        $idProperty = $reflectionClass->getProperty('id');
        $idProperty->setAccessible(true);
        $idProperty->setValue($object, $this->primaryIdForPersitingObject);
    }

    public function flush() { }

    ...
}

Once you implemented this, you can inject the instance of MyEntityManager and make your tests small and easier to maintain.

You test would look like

<?php

class BookmarkManagerTest extends \PHPUnit_Framework_TestCase
{
    public function testThatRestaurantAdditionToBookmarksIsWellManaged()
    {
        // ...
        $entityManager = MyEntityManager(1);
        //...
        $bookManager = new BookmarkManager($tokenStorageMock, $entityManager, $sessionMock);
        //...
    }
}

Of course, a situation may be harder if there is a need of setting different ids for many persisting objects. Then you can, for example, increase $primaryIdForPersitingObject on persist call

public function persist($object)
{
    $reflectionClass = new ReflectionClass(get_class($object));
    $idProperty = $reflectionClass->getProperty('id');
    $idProperty->setAccessible(true);
    $idProperty->setValue($object, $this->primaryIdForPersitingObject);

    $this->primaryIdForPersitingObject++;
}

It may be extended even further to have separate primaryIdForPersitingObject each entity class, and your tests will be still clean.