8
votes

I'm having issues trying to unit test an action which uses ZfcUser for authentication. I need some way to mock the ZfcUser Controller plugin but I'm not so sure how to do this. I've managed to successfully produce some unit tests for tables and models but the controller requires a lot of injected objects and is causing problems. Does anyone know how to set up the ZfcUser mocks to successfully unit test a controller?

Here is my test (copied from the ZF2 tutorial):

<?php
namespace SmsTest\Controller;

use SmsTest\Bootstrap;
use Sms\Controller\SmsController;
use Zend\Http\Request;
use Zend\Http\Response;
use Zend\Mvc\MvcEvent;
use Zend\Mvc\Router\RouteMatch;
use Zend\Mvc\Router\Http\TreeRouteStack as HttpRouter;
use PHPUnit_Framework_TestCase;

class SmsControllerTest extends PHPUnit_Framework_TestCase
{
    protected $controller;
    protected $request;
    protected $response;
    protected $routeMatch;
    protected $event;

    protected function setUp()
    {

        $serviceManager = Bootstrap::getServiceManager();

        $this->controller = new SmsController();
        $this->request    = new Request();
        $this->routeMatch = new RouteMatch(array('controller' => 'index'));
        $this->event      = new MvcEvent();
        $config = $serviceManager->get('Config');
        $routerConfig = isset($config['router']) ? $config['router'] : array();
        $router = HttpRouter::factory($routerConfig);
        $this->event->setRouter($router);
        $this->event->setRouteMatch($this->routeMatch);
        $this->controller->setEvent($this->event);
        $this->controller->setServiceLocator($serviceManager);
    }


    /* Test all actions can be accessed */

    public function testIndexActionCanBeAccessed()
    {
        $this->routeMatch->setParam('action', 'index');

        $result   = $this->controller->dispatch($this->request);
        $response = $this->controller->getResponse();

        $this->assertEquals(200, $response->getStatusCode());
    }
}

I tried the following in the setUp method:

    $mockAuth = $this->getMock('ZfcUser\Entity\UserInterface');


    $authMock = $this->getMock('Zend\Authentication\AuthenticationService');
    $authMock->expects($this->any())
         ->method('hasIdentity')
         ->will($this->returnValue(true));

    $authMock->expects($this->any())
         ->method('getIdentity')
         ->will($this->returnValue(array('user_id' => 1)));

But I'm not sure how to inject this in to the controller instance.

Lets pretend my index action code is just as follows:

public function indexAction() {
    //Check if logged in
    if (!$this->zfcUserAuthentication()->hasIdentity()) {
        return $this->redirect()->toRoute('zfcuser/login');
    }

    return new ViewModel(array(
        'success' => true,
    ));
}

Test Results:

1) SmsTest\Controller\SmsControllerTest::testIndexActionCanBeAccessed
Zend\ServiceManager\Exception\ServiceNotFoundException: Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for zfcUserAuthentication

/var/www/soap-app.localhost/Zend/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:450
/var/www/soap-app.localhost/Zend/vendor/zendframework/zendframework/library/Zend/ServiceManager/AbstractPluginManager.php:110
/var/www/soap-app.localhost/Zend/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/PluginManager.php:90
/var/www/soap-app.localhost/Zend/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php:276
/var/www/soap-app.localhost/Zend/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php:291
/var/www/soap-app.localhost/Zend/module/Sms/src/Sms/Controller/SmsController.php:974
/var/www/soap-app.localhost/Zend/module/Sms/src/Sms/Controller/SmsController.php:974
/var/www/soap-app.localhost/Zend/module/Sms/src/Sms/Controller/SmsController.php:158
/var/www/soap-app.localhost/Zend/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractActionController.php:87
/var/www/soap-app.localhost/Zend/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/var/www/soap-app.localhost/Zend/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:208
/var/www/soap-app.localhost/Zend/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php:108
/var/www/soap-app.localhost/Zend/module/Sms/test/SmsTest/Controller/SmsControllerTest.php:57

The line which causes this exception is the controller is: if (!$this->zfcUserAuthentication()->hasIdentity()) { That line relates to line 974 in the SmsController.

It's obvious I don't have access to the ZfcUserAuthentication service, so the question is, How do I mock the ZfcUserAuthentication service and inject it in to my Controller?

To continue the theme how would I go about mocking a logged in user to successfully test my action is working to specification?

2

2 Answers

10
votes

The ZfcUser documentation suggests that this is a plugin so you need to inject this into the controller.

You will need to amend your class names to pick up the ZfcUser classes

Your mocks will also need to be addapted as getIdenty returns a different object.

The following worked for me - insert in your phpunit setUp() method.

$serviceManager = Bootstrap::getServiceManager();
$this->controller = new RegisterController();
$this->request    = new Request();
$this->routeMatch = new RouteMatch(array('controller' => 'add'));
$this->event      = new MvcEvent();
$config = $serviceManager->get('Config');
$routerConfig = isset($config['router']) ? $config['router'] : array();
$router = HttpRouter::factory($routerConfig);
$this->event->setRouter($router);
$this->event->setRouteMatch($this->routeMatch);
$this->controller->setEvent($this->event);
$this->controller->setServiceLocator($serviceManager);
$mockAuth = $this->getMock('ZfcUser\Entity\UserInterface');

$ZfcUserMock = $this->getMock('ZfcUser\Entity\User');  

$ZfcUserMock->expects($this->any())
            ->method('getId')
            ->will($this->returnValue('1'));

$authMock = $this->getMock('ZfcUser\Controller\Plugin\ZfcUserAuthentication');

$authMock->expects($this->any())
         ->method('hasIdentity')
            -> will($this->returnValue(true));  

$authMock->expects($this->any())
         ->method('getIdentity')
         ->will($this->returnValue($ZfcUserMock));

$this->controller->getPluginManager()
     ->setService('zfcUserAuthentication', $authMock);

There may be an easier way would welcome other thoughts.

0
votes

This is how I did it.

<?php

namespace IssueTest\Controller;

use Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;

class IssueControllerTest extends AbstractHttpControllerTestCase
{
    protected $serviceManager;

    public function setUp()
    {
        $this->setApplicationConfig(
            include '/media/policybubble/config/application.config.php'
        );
        parent::setUp();
        $ZfcUserMock = $this->getMock('ZfcUser\Entity\User');
        $ZfcUserMock->expects($this->any())
            ->method('getId')
            ->will($this->returnValue('1'));
        $authMock = $this->getMock(
            'ZfcUser\Controller\Plugin\ZfcUserAuthentication'
        );
        $authMock->expects($this->any())
            ->method('hasIdentity')
            ->will($this->returnValue(true));
        $authMock->expects($this->any())
            ->method('getIdentity')
            ->will($this->returnValue($ZfcUserMock));
        $this->serviceManager = $this->getApplicationServiceLocator();
        $this->serviceManager->setAllowOverride(true);
        $this->serviceManager->get('ControllerPluginManager')->setService(
            'zfcUserAuthentication', $authMock
        );
    }

    public function testIndexActionCanBeAccessed()
    {
        $this->dispatch('/issue');
        $this->assertResponseStatusCode(200);
        $this->assertModuleName('Issue');
        $this->assertControllerName('Issue\Controller\Issue');
        $this->assertControllerClass('IssueController');
        $this->assertMatchedRouteName('issue');
    }

    public function testAddActionRedirectsAfterValidPost()
    {
        $issueTableMock = $this->getMockBuilder('Issue\Model\IssueTable')
            ->disableOriginalConstructor()
            ->getMock();
        $issueTableMock->expects($this->once())
            ->method('saveIssue')
            ->will($this->returnValue(null));
        $this->serviceManager->setService('Issue\Model\IssueTable', $issueTableMock);
        $postData = array(
            'title' => 'Gun Control',
            'id'    => '',
        );
        $this->dispatch('/issue/add', 'POST', $postData);
        $this->assertResponseStatusCode(302);
        $this->assertRedirectTo('/issue');
    }

    public function testEditActionRedirectsAfterValidPost()
    {
        $issueTableMock = $this->getMockBuilder('Issue\Model\IssueTable')
            ->disableOriginalConstructor()
            ->getMock();
        $issueTableMock->expects($this->once())
            ->method('saveIssue')
            ->will($this->returnValue(null));
        $this->serviceManager->setService('Issue\Model\IssueTable', $issueTableMock);
        $issueTableMock->expects($this->once())
            ->method('getIssue')
            ->will($this->returnValue(new \Issue\Model\Issue()));
        $postData = array(
            'title' => 'Gun Control',
            'id'    => '1',
        );
        $this->dispatch('/issue/edit/1', 'POST', $postData);
        $this->assertResponseStatusCode(302);
        $this->assertRedirectTo('/issue');
    }

    public function testDeleteActionRedirectsAfterValidPost()
    {
        $postData = array(
            'title' => 'Gun Control',
            'id'    => '1',
        );
        $this->dispatch('/issue/delete/1', 'POST', $postData);
        $this->assertResponseStatusCode(302);
        $this->assertRedirectTo('/issue');
    }
}

<?php

namespace Issue\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Issue\Model\Issue;
use Issue\Form\IssueForm;

class IssueController extends AbstractActionController
{
    protected $issueTable;

    public function indexAction()
    {
        if (!$this->zfcUserAuthentication()->hasIdentity()) {
            return;
        }
        return new ViewModel(
            array(
                'issues' => $this->getIssueTable()->fetchAll(
                    $this->zfcUserAuthentication()->getIdentity()->getId()
                ),
            )
        );
    }

    public function addAction()
    {
        if (!$this->zfcUserAuthentication()->hasIdentity()) {
            return $this->redirect()->toRoute('issue');
        }
        $form = new IssueForm();
        $form->get('submit')->setValue('Add');
        $request = $this->getRequest();
        if ($request->isPost()) {
            $issue = new Issue();
            $form->setInputFilter($issue->getInputFilter());
            $form->setData($request->getPost());
            if ($form->isValid()) {
                $issue->exchangeArray($form->getData());
                $this->getIssueTable()->saveIssue(
                    $issue,
                    $this->zfcUserAuthentication()->getIdentity()->getId()
                );
                // Redirect to list of issues
                return $this->redirect()->toRoute('issue');
            }
        }
        return array('form' => $form);
    }

    public function editAction()
    {
        if (!$this->zfcUserAuthentication()->hasIdentity()) {
            return $this->redirect()->toRoute('issue');
        }
        $id = (int)$this->params()->fromRoute('id', 0);
        if (!$id) {
            return $this->redirect()->toRoute(
                'issue', array(
                'action' => 'add'
            )
            );
        }
        // Get the Issue with the specified id.  An exception is thrown
        // if it cannot be found, in which case go to the index page.
        try {
            $issue = $this->getIssueTable()->getIssue($id);
        } catch (\Exception $ex) {
            return $this->redirect()->toRoute(
                'issue', array(
                'action' => 'index'
            )
            );
        }
        $form = new IssueForm();
        $form->bind($issue);
        $form->get('submit')->setAttribute('value', 'Edit');
        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setInputFilter($issue->getInputFilter());
            $form->setData($request->getPost());
            if ($form->isValid()) {
                $this->getIssueTable()->saveIssue(
                    $issue,
                    $this->zfcUserAuthentication()->getIdentity()->getId()
                );
                // Redirect to list of issues
                return $this->redirect()->toRoute('issue');
            }
        }
        return array(
            'id'   => $id,
            'form' => $form,
        );
    }

    public function deleteAction()
    {
        if (!$this->zfcUserAuthentication()->hasIdentity()) {
            return $this->redirect()->toRoute('issue');
        }
        $id = (int)$this->params()->fromRoute('id', 0);
        if (!$id) {
            return $this->redirect()->toRoute('issue');
        }
        $request = $this->getRequest();
        if ($request->isPost()) {
            $del = $request->getPost('del', 'No');
            if ($del == 'Yes') {
                $id = (int)$request->getPost('id');
                $this->getIssueTable()->deleteIssue($id);
            }
            // Redirect to list of issues
            return $this->redirect()->toRoute('issue');
        }
        return array(
            'id'    => $id,
            'issue' => $this->getIssueTable()->getIssue($id)
        );
    }

    public function getIssueTable()
    {
        if (!$this->issueTable) {
            $sm = $this->getServiceLocator();
            $this->issueTable = $sm->get('Issue\Model\IssueTable');
        }
        return $this->issueTable;
    }
}