Mock testing in Zend with Doctrine: mimicking the mocking

A few notes about mock testing in Zend with Doctrine.

There is great material already about this topic (such as this, this, and this).

  • The goal is mocking the retrieval of the entity manager to avoid making a mess with the existing database.

“unable to create an instance for doctrine.entity_manager.orm_default”

The Bootstrap.php may be a place to fix the error “unable to create an instance for doctrine.entity_manager.orm_default” via adding the following line. It seems to “force” the service manager to initialize the instance.

$serviceManager->get('doctrine.entity_manager.orm_default');

However, a nice solution was to create the service via a constructor that receives both the service manger, and the entity manager. The service is not created in the controller. Instead, it should be placed in the Module.php file for the module. The service will then almost-never need to be created via ‘new’. This is how it looks like in Module.php:

public function getServiceConfig()
{
   return array(
      'factories' => array(
         'BagelService' =>  function($sm)
         {
            $em = $sm->get('doctrine.entitymanager.orm_default');
            return new BagelService($sm, $em);
         }
      ),
   );
}

This facilitates mocking the entity manager. However, I did not follow that route.

Minor note: in our setup there is something not 100% perfect, which requires me to utilize ‘doctrine.entitymanager.orm_default’, which does not have an underscore between the words entity and manager.

My setup was to initialize the service passing only the service manager to its constructor. It is normally initialized inside of a controller. There is nothing about the service in the Module.php file.

Example of test setup and actual test.

Initialization in the setUp function is done through the Bootstrap class.

protected function setUp()
{
   $this->serviceManager = Bootstrap::getServiceManager();
   $this->bagelService = new BagelService($this->serviceManager);
   parent::setUp();
}

Example test:

public function testBagelPromotion()
{
   $response = $this->bagelService->getPromotion();
   $this->assertEquals('Buy 1 bagel, get free coffee', $response);
}

In some tests, I’ve grouped several asserts together using an array as follows:

public function testObjectInitiallyEmptyValues()
{
    $fields = ['Items', 'Payments', 'Attachments'];
    $obj = new EntityABCDEF();
    foreach($fields as $each) {
        $this->assertCount(0, $obj->{'get' . $each }(),
           "'$each' should initially have zero elements.");
    }
}

Example test mocking the entity manager:

public function testGetBagelById()
{
   // Create a dummy object
   $object = new BagelEntity();
   $object->setBagelNote('BAM12345');

   // Mock the entity manager
   $emMock = $this->getMock('EntityManager',
      ['getRepository', 'getClassMetadata', 'persist', 'flush', 'find'], [], '', false);

   // Create a mocked service, only mocking one method
   $mockedService = $this->getMockBuilder(BagelService::class)
      ->setConstructorArgs([$this->serviceManager])
      ->setMethods(['getEntityManager'])
      ->getMock();

   // setup the expectation of the method we are mocking
   $mockedService->expects($this->any)
      ->method('getEntityManager')
      ->will($this->returnValue($emMock));

   // 'find' should return our dummy object
   $emMock->expects( $this->any() )
      ->method('find')
      ->will($this->returnValue($object));

   // Call the mocked service requesting an object
   $response = $mockedService->getBagelById( 12 );

   // Check the response is the correct type of entity
   $this->assertInstanceOf(BagelEntity::class, $response);

   // Check that the returned entity is our dummy object
   $this->assertEquals('BAM12345', $response->getBagelNote());
}

There were tests where I did not utilize the mock entity manager because the functionality being tested did not require it. Examples: testing that requesting an entity by ID with an invalid ID returns null, and testing that QueryBuilder was generating a query correctly.

Why was all of this helpful?

  • I found out that we have functions that are repeated in other classes. My suggestion to the team lead (when she comes back from vacation) will be to have such functions moved to a Utility class.
  • I had to move some strings constants as actual defined constants in order to test the output of a few methods to avoid having the test-case break in case that our customer wants us to change something such as a description of an error message.