2

In my PHP application I am using Doctrine ORM in the data access later. In this layer I have services such as repositories that typically use Doctrines EntityManager. In those services the methods that do some form of modification typically follow this pattern:

public function modifyStuff( /* ... */ ) {
    try {
        $stuff = $this->entityManager->find( /* ... */ )
    }
    catch ( ORMException $ex ) {
        /* ... */
    }

    // Poke at $stuff

    try {
        $this->entityManager->persist( $stuff );
        $this->entityManager->flush();
    }
    catch ( ORMException $ex ) {
        /* code that needs to be tested */
    }
}

I'm trying to find a way to test the code in the second catch block: the code that handles write failures. So in my test I need to make the EntityManager throw when something is written. And naturally I want to minimize binding in my test to both the implementation of this repository (ie which doctrine methods are used) and the EntityManager interface and implementation itself. Ideally I could do something like

$entityManager = new TestEntityManager();
$entityManager->throwOnWrite();

after which the EntityManager would function normally except that it would throw on write. (I have test doubles like that for my repositories.)

I tried using the PHPUnit mock API as follows:

$entityManager = $this->getMockBuilder( EntityManager::class )->disableOrgninalConstructor()->getMock()
$entityManager->expects( $this->any() )
    ->method( 'persist' )
    ->willThrowException( new ORMException() );

This is not ideal since now my test binds to persist method, though this is not that big of an issue. This does not work though since for the service to function its constructor needs some arguments. Then I tried

$entityManager =
    $this->getMockBuilder( EntityManager::class )
        ->setConstructorArgs( [
            $this->entityManager->getConnection(),
            $this->entityManager->getConfiguration(),
            $this->entityManager->getEventManager()
        ] )
        ->getMock();

And found out that the EntityManager constructor is not public. So it seems like I won't be able to use the PHPUnit mocking API.

Any ideas on how to make the EntityManager throw on write, or otherwise test the code supposed to handle that case?

Jeroen De Dauw
  • 10,321
  • 15
  • 56
  • 79
  • 1
    Maybe https://stackoverflow.com/a/43083956/5769763? Posting as a comment as I don't have time to try to actually verify this is what you mean and if that works. – Michał Łazowik Feb 20 '18 at 23:12
  • That approach is made impossible by the service needing its constructor parameters and PHPUnit apparently not being able to stuff constructor arguments into a non-public constructor. – Jeroen De Dauw Feb 21 '18 at 23:04

1 Answers1

4

You can simulate write failure by throwing such exception from onFlush event handler. Consider this example:

$p = new Entity\Product();
$p->setName("Test Product");
$em->persist($p);
$em->flush();

$em->getEventManager()->addEventListener(\Doctrine\ORM\Events::onFlush, new WriteErrorListener());


// Nothing changed, no exceptions.
$p->setName("Test Product");
$em->flush();

try {
    $p->setName("Name changed");
    $em->flush();
    echo "\n\n    UNEXPECTED\n\n";
} catch (\Doctrine\ORM\ORMException $ex) {
    echo $ex->getMessage() . "\n";
}

WriteErrorListener:

use \Doctrine\ORM\Query\QueryException;

class WriteErrorListener 
{
    public function onFlush(\Doctrine\ORM\Event\OnFlushEventArgs $eventArgs)
    {
        $em = $eventArgs->getEntityManager();
        $uow = $em->getUnitOfWork();

        foreach ($uow->getScheduledEntityInsertions() as $entity) {
            // You can inspect $entity and provide additional information
            throw new QueryException('[Test condition] Insertion of entity pending');
        }

        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            throw new QueryException('[Test condition] Update of entity pending');
        }
        // And so on...
    }
}

Run:

$ php app.php 
[Test condition] Update of entity pending
Tns
  • 390
  • 1
  • 7
  • 1
    This approach has more binding than is ideal but it works and is not quite as horrible as some of the routes I was going down. You can see my implementation here: https://github.com/wmde/fundraising-memberships/pull/5/commits/a3c93a730ddb7bef1d4aac0ed780ea4e5fb97798 – Jeroen De Dauw Feb 26 '18 at 12:01