ZF2 Module wich allows to inject dependencies in controller action for better incapsulate testing and remove controller dependency from ServiceLocator.
Problem: I have simply CRUD Controller, with actions "create", "update", "show", "delete", i have 3 dependency in action "delete": ViewModel, Request, some Service.
class AjaxController extends Zend\Mvc\Controller\AbstractActionController
{
public function deleteTimesheetAction() {
$view = new ViewModel();
if (!$this->getRequest()->isPost()) {
return $view;
}
$timesheetDeleteService = $this->getServiceLocator()->get('Timesheet\Timesheet\Service\Delete');
$timesheetId = $this->getRequest()->getPost()->get('id', 0);
if (!$timesheetDeleteService->delete($timesheetId)) {
$view->errors = $timesheetDeleteService->getErrors();
}
return $view;
}
//...
}in this case i can't easy test this controller, because:
- I can't mock ViewModel (constructor calling)
- Very difficult create test for this $this->getRequest()->getPost()->get('id', 0);
- Nobody understand dependencies in current controller, because $this->getServiceLocator()->get('SomeService') inside controller - is bad practice
Ok, refactor it..
Problem: I have simply CRUD Controller, with actions "create", "update", "show", "delete", i have 3 dependency in action "delete": ViewModel, Request, some Service. For height testability i add all dependencies in Controller::__constructor().
class AjaxController extends Zend\Mvc\Controller\AbstractActionController
{
/**
* @var BaseFinder
*/
private $timesheetFinder;
/**
* @var BaseFinder
*/
private $calendarFinder;
/**
* @var CreateInterface
*/
private $createService;
/**
* @var UpdateInterface
*/
private $updateService;
/**
* @var DeleteInterface
*/
private $deleteService;
/**
* @var AjaxViewModel
*/
private $view;
public function __construct(
BaseFinder $timesheetFinder,
BaseFinder $calendarFinder,
CreateInterface $timesheetCreateService,
UpdateInterface $timesheetUpdateService,
DeleteInterface $timesheetDeleteService,
AjaxViewModel $view)
{
$this->timesheetFinder = $timesheetFinder;
$this->calendarFinder = $calendarFinder;
$this->createService = $timesheetCreateService;
$this->updateService = $timesheetUpdateService;
$this->deleteService = $timesheetDeleteService;
$this->view = $view;
}
public function deleteTimesheetAction() {
$view = new ViewModel();
if (!$this->getRequest()->isPost()) {
return $this->view;
}
$timesheetId = $this->getRequest()->getPost()->get('id', 0);
if (!$this->deleteService->delete($timesheetId)) {
$this->view->errors = $this->deleteService->getErrors();
}
return $this->view;
}
// ...
}class AjaxControllerFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $serviceLocator) {
$serviceManager = $serviceLocator->getServiceLocator();
return new AjaxController(
$serviceManager->get('Timesheet\Timesheet\Service\Finder'),
$serviceManager->get('Calendar\Calendar\Service\Finder'),
$serviceManager->get('Timesheet\Timesheet\Service\Create'),
$serviceManager->get('Timesheet\Timesheet\Service\Update'),
$serviceManager->get('Timesheet\Timesheet\Service\Delete'),
$serviceManager->get('Timesheet\Controller\ViewModel\AjaxViewModel')
);
}
}in this case i can easy test this controller, but:
- I have to big __constructor (almost god object)
- How many mock's i must create for test on method "delete"?
- Nobody understand where i use each dependency in current controller
- I must test ControllerFactory
Solution: use t4web/ActionInjections
Add in your module.config.php section controller_action_injections
'controller_action_injections' => array(
'Timesheet\Controller\User\AjaxController' => array(
'deleteTimesheetAction' => array(
'request',
'Timesheet\Controller\ViewModel\AjaxViewModel',
'Timesheet\Timesheet\Service\Delete',
),
),
),where request, Timesheet\Controller\ViewModel\AjaxViewModel, Timesheet\Timesheet\Service\Delete your dependecies, and just use it in your controller action
class AjaxController extends Zend\Mvc\Controller\AbstractActionController
{
public function deleteTimesheetAction(HttpRequest $request, AjaxViewModel $view, DeleteInterface $timesheetDeleteService) {
if (!$request->isPost()) {
return $view;
}
$timesheetId = $request->getPost()->get('id', 0);
if (!$timesheetDeleteService->delete($timesheetId)) {
$view->setErrors($timesheetDeleteService->getErrors());
}
return $view;
}
//...
}and test it:
class AjaxControllerTest extends \PHPUnit_Framework_TestCase {
public function testDeleteTimesheetAction_Delete_ReturnView() {
$requestMock = $this->getMockBuilder('Zend\Http\PhpEnvironment\Request')->disableOriginalConstructor()->getMock();
$timesheetDeleteServiceMock = $this->getMockBuilder('T4webBase\Domain\Service\Delete')->disableOriginalConstructor()->getMock();
$ajaxViewModel = new AjaxViewModel();
$timesheetId = 1;
$parameters = new Parameters(array('id' => $timesheetId));
$requestMock->expects($this->once())
->method('isPost')
->will($this->returnValue(true));
$requestMock->expects($this->once())
->method('getPost')
->will($this->returnValue($parameters));
$timesheetDeleteServiceMock->expects($this->once())
->method('delete')
->with($this->equalTo($timesheetId))
->will($this->returnValue(true));
$controller = new AjaxController();
/** @var $result AjaxViewModel */
$result = $controller->deleteTimesheetAction($requestMock, $ajaxViewModel, $timesheetDeleteServiceMock);
$this->assertEquals($ajaxViewModel, $result);
}
//...
}very fast, easy, readable, incapsulate unit test.
- Zend Framework 2 (latest master)
Clone this project into your ./vendor/ directory.
Add this project in your composer.json:
"repositories": [
{
"type": "git",
"url": "https://github.com/t4web/actioninjections.git"
}
],
"require": {
"t4web/actioninjections": "dev-master"
}Now tell composer to download Authentication by running the command:
$ php composer.phar updateNot need enabling it in your application.config.phpfile, just extends from T4webActionInjections\Mvc\Controller\AbstractActionController
Unit test runnig from authentication module directory.
$ phpunit
