如何将模拟类注入控制器(无需控制器知道测试)

时间:2011-06-08 02:19:00

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

这是我之前提出的问题的后续内容: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());
        }
    }
}

这似乎不对......

3 个答案:

答案 0 :(得分:2)

这是错误的,您的被测系统根本不应该对模拟对象有任何了解。

幸运的是,因为你有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()方法中,这样就不必为每次测试重复此代码。

答案 1 :(得分:0)

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

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实例

答案 2 :(得分:0)

下面是我为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);
}

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