t4web/actioninjections

ZF2 模块,允许在控制器动作中注入依赖,以更好地封装测试并从 ServiceLocator 中移除控制器依赖。

1.0.1 2015-10-24 18:50 UTC

This package is auto-updated.

Last update: 2024-08-26 17:39:17 UTC


README

主分支: Build Status codecov.io Scrutinizer Code Quality SensioLabsInsight Dependency Status

介绍

ZF2 模块,允许在控制器动作中注入依赖,以更好地封装测试并从 ServiceLocator 中移除控制器依赖。

问题:我有一个简单的 CRUD 控制器,具有“创建”、“更新”、“显示”、“删除”操作,在“删除”操作中有 3 个依赖:ViewModel、Request 和一些 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;
    }
    //...
}

在这种情况下,我无法轻松测试此控制器,因为

  1. 我无法模拟 ViewModel(构造函数调用)
  2. 创建针对此 $this->getRequest()->getPost()->get('id', 0); 的测试非常困难
  3. 没有人理解当前控制器中的依赖,因为在控制器中 $this->getServiceLocator()->get('SomeService') 是一种不良做法

好吧,重构它...

问题:我有一个简单的 CRUD 控制器,具有“创建”、“更新”、“显示”、“删除”操作,在“删除”操作中有 3 个依赖:ViewModel、Request 和一些 Service。为了提高可测试性,我在 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')
        );
    }
}

在这种情况下,我可以轻松测试此控制器,但是

  1. 我的 __constructor 太大(几乎是全能对象)
  2. 我必须为“删除”方法创建多少个模拟来测试?
  3. 没有人理解当前控制器中每个依赖的使用方式
  4. 我必须测试 ControllerFactory

解决方案:使用 t4web/ActionInjections 在您的 module.config.php 部分添加 controller_action_injections

    'controller_action_injections' => array(
        'Timesheet\Controller\User\AjaxController' => array(
            'deleteTimesheetAction' => array(
                'request',
                'Timesheet\Controller\ViewModel\AjaxViewModel',
                'Timesheet\Timesheet\Service\Delete',
            ),
        ),
    ),

其中包含 requestTimesheet\Controller\ViewModel\AjaxViewModelTimesheet\Timesheet\Service\Delete 您的依赖项,并在您的控制器动作中使用它们

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;
    }
    //...
}

并测试它们

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);
    }
    //...
}

非常快、简单、易于阅读、封装的单元测试。

要求

安装

主要设置

通过克隆项目

将此项目克隆到您的 ./vendor/ 目录中。

使用 composer

在您的 composer.json 中添加此项目

"repositories": [
        {
            "type": "git",
            "url": "https://github.com/t4web/actioninjections.git"
        }
],

"require": {
    "t4web/actioninjections": "dev-master"
}

现在运行命令让 composer 下载 Authentication

$ php composer.phar update

安装后

无需在您的 application.config.php 文件中启用它,只需扩展 T4webActionInjections\Mvc\Controller\AbstractActionController

测试

单元测试从 authentication 模块目录运行。

$ phpunit