Как внедрить фиктивные классы в контроллеры (без информирования контроллера о тестах) - PullRequest
2 голосов
/ 08 июня 2011

Это продолжение предыдущего вопроса, который у меня был: Как лучше отделить слой данных и ограничить объем моих модульных тестов?

Я прочитал о Zend и DI / IoC и предложил следующие изменения в моем коде:

Модуль Bootstrap

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
    }
}

Итак, теперь контроллеру все равно, к каким конкретно классам относятся мапперы - до тех пор, пока существует маппер ...

Но как теперь заменить эти классы на mocks для модульного тестирования, не уведомляя приложение / контроллер о том, что он тестируется? Все, что я могу придумать, - это вставить что-то в помощник действий для обнаружения текущая среда приложения и загрузка макетов напрямую:

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 ]

2 голосов
/ 08 июня 2011

Это неправильно, ваша тестируемая система вообще не должна знать о фиктивных объектах.

К счастью, потому что у вас есть 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 (), чтобы вам не приходилось повторять его для каждого теста.

0 голосов
/ 26 сентября 2012

Ниже приведено мое решение для введения поддельной временной метки для модульного теста ControllerTest, которая аналогична вопросу, первоначально опубликованному выше.

В классе ControllerTest создается экземпляр $ mockDateTime и добавляется в качестве параметра вFrontController перед вызовом dispatch ().

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

MajorПреимущество этого решения заключается в том, что оно позволяет внедрять зависимости, в то время как для очистки глобального состояния не требуется модульных тестов.

0 голосов
/ 10 июня 2011

Итак, после нескольких промахов я решил переписать хелпер действий:

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());
}

И я удостоверяюсь, что я очищаю экземпляр Zend_Registry по умолчанию в моем tearDown ()

...