【问题标题】:How to inject mock classes into controllers (without having the controller aware of the tests)如何将模拟类注入控制器(不让控制器知道测试)
【发布时间】:2011-09-10 12:57:47
【问题描述】:

这是我上一个问题的后续问题:How to decouple my data layer better and restrict the scope of my unit tests?

我阅读了有关 Zend 和 DI/IoC 的文章,并对我的代码进行了以下更改:

模块引导

class Api_Bootstrap extends Zend_Application_Module_Bootstrap
{
    protected function _initAllowedMethods()
    {
        $front = Zend_Controller_Front::getInstance();
        $front->setParam('api_allowedMethods', array('POST'));
    }

    protected function _initResourceLoader()
    {
        $resourceLoader = $this->getResourceLoader();
        $resourceLoader->addResourceType('actionhelper', 'controllers/helpers', 'Controller_Action_Helper');
    }

    protected function _initActionHelpers()
    {
        Zend_Controller_Action_HelperBroker::addHelper(new Api_Controller_Action_Helper_Model());
    }
}

动作助手

class Api_Controller_Action_Helper_Model extends Zend_Controller_Action_Helper_Abstract
{
    public function preDispatch()
    {
        if ($this->_actionController->getRequest()->getModuleName() != 'api') {
            return;
        }

        $this->_actionController->addMapper('account', new Application_Model_Mapper_Account());
        $this->_actionController->addMapper('product', new Application_Model_Mapper_Product());
        $this->_actionController->addMapper('subscription', new Application_Model_Mapper_Subscription());
    }
}

控制器

class Api_AuthController extends AMH_Controller
{
    protected $_mappers = array();

    public function addMapper($name, $mapper)
    {
        $this->_mappers[$name] = $mapper;
    }

    public function validateUserAction()
    {
        // stuff

        $accounts = $this->_mappers['account']->find(array('username' => $username, 'password' => $password));

        // stuff
    }
}

所以,现在,控制器并不关心映射器是什么特定的类——只要有一个映射器......

但是我现在如何用模拟替换这些类以进行单元测试,而不让应用程序/控制器知道它正在被测试?我能想到的就是在动作助手中添加一些东西来检测当前应用环境并直接加载模拟:

class Api_Controller_Action_Helper_Model extends Zend_Controller_Action_Helper_Abstract
{
    public function preDispatch()
    {
        if ($this->_actionController->getRequest()->getModuleName() != 'api') {
            return;
        }

        if (APPLICATION_ENV != 'testing') {
            $this->_actionController->addMapper('account', new Application_Model_Mapper_Account());
            $this->_actionController->addMapper('product', new Application_Model_Mapper_Product());
            $this->_actionController->addMapper('subscription', new Application_Model_Mapper_Subscription());
        } else {
            $this->_actionController->addMapper('account', new Application_Model_Mapper_AccountMock());
            $this->_actionController->addMapper('product', new Application_Model_Mapper_ProductMock());
            $this->_actionController->addMapper('subscription', new Application_Model_Mapper_SubscriptionMock());
        }
    }
}

这似乎是错误的......

【问题讨论】:

  • 只要您在代码中硬编码类名以进行实例化(和单例访问),我认为很难(呃)注入模拟。

标签: php unit-testing zend-framework dependency-injection phpunit


【解决方案1】:

错了,你的被测系统根本不应该有任何关于模拟对象的知识。

谢天谢地,因为您有 DI,所以不必这样做。只需在测试中实例化您的对象,然后使用 addMapper() 将默认映射器替换为模拟版本。

您的测试用例应该类似于:

public function testBlah()
{
  $helper_model = new Api_Controller_Action_Helper_Model;
  $helper_model->_actionController->addMapper('account', new Application_Model_Mapper_AccountMock());
  $helper_model->_actionController->addMapper('product', new Application_Model_Mapper_ProductMock());
  $helper_model->_actionController->addMapper('subscription', new Application_Model_Mapper_SubscriptionMock());

  // test code...
}

您也可以将此代码放在您的 setUp() 方法中,这样您就不必为每次测试重复它。

【讨论】:

    【解决方案2】:

    所以,在几次失误之后,我决定重写动作助手:

    class Api_Controller_Action_Helper_Model extends Zend_Controller_Action_Helper_Abstract
    {
        public function preDispatch()
        {
            if ($this->_actionController->getRequest()->getModuleName() != 'api') {
                return;
            }
    
            $registry = Zend_Registry::getInstance();
            $mappers = array();
            if ($registry->offsetExists('mappers')) {
                $mappers = $registry->get('mappers');
            }
    
            $this->_actionController->addMapper('account', (isset($mappers['account']) ? $mappers['account'] : new Application_Model_Mapper_Account()));
            $this->_actionController->addMapper('product', (isset($mappers['product']) ? $mappers['product'] : new Application_Model_Mapper_Product()));
            $this->_actionController->addMapper('subscription', (isset($mappers['subscription']) ? $mappers['subscription'] : new Application_Model_Mapper_Subscription()));
        }
    }
    

    这意味着我可以通过注册表注入我喜欢的任何类,但对实际的映射器有一个默认/回退。

    我的测试用例是:

    public function testPostValidateAccount($message)
    {
        $request = $this->getRequest();
        $request->setMethod('POST');
        $request->setRawBody(file_get_contents($message));
    
        $account = $this->getMock('Application_Model_Account');
    
        $accountMapper = $this->getMock('Application_Model_Mapper_Account');
        $accountMapper->expects($this->any())
            ->method('find')
            ->with($this->equalTo(array('username' => 'sjones', 'password' => 'test')))
            ->will($this->returnValue($accountMapper));
        $accountMapper->expects($this->any())
            ->method('count')
            ->will($this->returnValue(1));
        $accountMapper->expects($this->any())
            ->method('offsetGet')
            ->with($this->equalTo(0))
            ->will($this->returnValue($account));
    
        Zend_Registry::set('mappers', array(
            'account' => $accountMapper,
        ));
    
        $this->dispatch('/api/auth/validate-user');
    
        $this->assertModule('api');
        $this->assertController('auth');
        $this->assertAction('validate-user');
        $this->assertResponseCode(200);
    
        $expectedResponse = file_get_contents(dirname(__FILE__) . '/_testPostValidateAccount/response.xml');
    
        $this->assertEquals($expectedResponse, $this->getResponse()->outputBody());
    }
    

    并且我确保我清除了我的 tearDown() 中的默认 Zend_Registry 实例

    【讨论】:

      【解决方案3】:

      下面是我为 ControllerTest 单元测试注入模拟时间戳的解决方案,这与上面最初发布的问题类似。

      在 ControllerTest 类中,$mockDateTime 被实例化,并在调用 dispatch() 之前作为参数添加到 FrontController。

      public function testControllerAction() {
          ....
          $mockDateTime = new DateTime('2011-01-01T12:34:56+10:30');
          $this->getFrontController()->setParam('datetime', $mockDateTime);
          $this->dispatch('/module/controller/action');
          ...
      }
      

      在 Controller 类中,dispatch() 会将任何参数传递给我们在这里扩展的 _setInvokeArgs():

      protected function _setInvokeArgs(array $args = array())
      {
          $this->_datetime = isset($args['datetime']) ? $args['datetime'] : new DateTime();
          return parent::_setInvokeArgs($args);
      }
      

      此解决方案的主要优点是它允许依赖注入,同时不需要单元测试来清理全局状态。

      【讨论】:

      猜你喜欢
      • 2012-05-03
      • 1970-01-01
      • 2016-08-19
      • 2016-02-21
      • 2018-12-01
      • 1970-01-01
      • 2016-05-11
      • 1970-01-01
      • 2016-06-02
      相关资源
      最近更新 更多