Pull to refresh

Тестирование контроллера в Symfony2

Reading time 5 min
Views 8.7K
Original author: Bulat Shakirzyanov
Предлагаю вашему вниманию перевод вчерашнего поста одного из разработчиков Symfony2 о подходе к unit-тестированию контроллеров в Symfony2. Тема очень актуальна для Symfony2 разработчиков. Также стоит отметить, что в посте упоминается результат дискуссии на dev-groups об использовании контроллера в роли сервиса в Symfony2.


Даже имея большой опыт работы с MVC фреймворками, одна вещь постоянно остается нераскрытой — как тестировать контроллеры. Я думаю, что основная причина этому — неочевидность тестирования, так как контроллеры относятся к сорту элементов «черной магии» фреймворка. Существует множество соглашений по поводу размещения контроллеров в файловой системе, о каких зависимостях он должен знать, и какие должны быть у контроллера жесткие связи (слой view).

Такая обстановка вещей не предполагает легких путей тестирования контроллеров, пока вы не можете выделить контроллер и некоторые его основные зависимости для тестирование взаимодействия — вам необходимо запустить весь фреймворк и запустить функциональные тесты.

Так как этот процесс довольно сложный и комплексный, люди обычно не прибегают к unit-тестированию контроллеров, функциональные тесты — это максимум что вы можете получить, но обычно вообще тестирования не происходит.

Symfony2 полностью меняет дело.

Изначально в фреймворке Symfony2 есть только соглашению по загрузке контроллера. Экземпляр контроллера остается очень легковесным и не требует для своей работы расширения некоторого родительского класса. Если ваши контроллеры реализуют интерфейс ContainerAware, вы получите DIC (dependency injection container) внедренный через метод ContainerAware::setContainer(), который вы можете использовать для доступа к любому сервису, который вы объявили в DIC.

Рекомендуемый метод тестирования контроллеров некоторое время был приближением тестирования черного ящика, когда вы тестируете полные запросы к приложению и проверяете вывод приблизительно так:
<?php
$client = $this->createClient();

$client->request('GET', '/index');
$response = $client->getResponse();

$this->assertEquals(200, $response->getStatusCode());
$this->assertRegExp('/<h1>My Cool Website<\/h1>/', $response->getContent());
Несмотря на то, что этот метод легко читаем и понятен, у него есть недостатки:
  • Для выполнения теста, нам нужно запустить ядро;
  • Это тестирует только тело ответа, что делает его очень чувствительным к изменению дизайна;
  • Как результат всего выше сказанного, он работает намного медленнее, и делает гораздо больше, чем это необходимо;
В идеальном мире, мне бы хотелось протестировать взаимодействие контроллера с другими сервисами в моем приложении, подобно этому:
<?php

namespace Company\ApplicationBundle\Tests\Controller;
use Company\ApplicationBundle\Controller\IndexController;

class IndexControllerTest extends \PHPUnit_Framework_TestCase
{
  //...
  public function testIndexAction()
  {
    $templating = $this->getMock('Symfony\Bundle\FrameworkBundle\Templating\Engine');
    $templating->expects($this->once())
      ->method('render')
      ->with('Application:Index:index')
      ->will($this->returnValue('success'))
    ;

    $controller = new IndexController();
    $controller->setTemplating($templating);

    $this->assertEquals('success', $controller->indexAction());
  }
}
Замечание: котроллер — сейчас это POPO (plain old PHP object) без базового класса, который он должен расширять. Symfony2 для работы ничего более не нужно кроме класса контроллера как такового для его работы.

Замечание: почитайте более о mock объектах в PHPUnit.

Хорошие новости состоят в том, что Symfony2 позволяет это. Теперь все ваши контроллеры могут выступать в роли сервисов. Прошлый, общепринятый вариант также поддерживается и незаменим для малых контроллеров, которые не требуют unit-тестирования.

Для того, чтобы контроллер из примера выше правильно взаимодействовал с Symfony2 и работал как следует, нам нужно следующее.

Создайте класс контроллера:
<?php

namespace Company\ApplicationBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Templating\Engine;

class IndexController
{
  /**
* @var Symfony\Bundle\FrameworkBundle\Templating\Engine
*/
  private $templating;

  /**
* @param Symfony\Bundle\FrameworkBundle\Templating\Engine $templating
*/
  public function setTemplating(Engine $templating)
  {
    $this->templating = $templating;
  }

  /**
* @return Symfony\Component\HttpFoundation\Response
*/
  public function indexAction()
  {
    return $this->templating->render('ApplicationBundle:Index:index');
  }
}

Создайте DIC конфигурацию, используя следующий xml:
<?xml version="1.0" ?>

<container xmlns="http://www.symfony-project.org/schema/dic/services"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services www.symfony-project.org/schema/dic/services/services-1.0.xsd">

  <services>
    <service id="index_controller" class="Company\ApplicationBundle\Controller\IndexController">
      <call method="setTemplating" />
        <argument type="service" id="templating" />
      </call>
    </service>
  </services>
</container>

Создайте конфигурацию роутинга:
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://www.symfony-project.org/schema/routing"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.symfony-project.org/schema/routing www.symfony-project.org/schema/routing/routing-1.0.xsd">

  <route id="index" pattern="/index">
    <default key="_controller">index_controller:indexAction</default>
  </route>
</routes>

Замечание: в примере выше, было использовано service_id:action вместо обычного BundleBundle:Controller:action (без суффикса ‘Action’).

Когда все это сделано, мы должны проинформировать Symfony2 о наших сервисах. Для того чтоб не произошло создание Dependency Injection расширения и создания точки конфигурационного файла, мы можем зарегистрировать наши сервисы напрямую:

<?php

namespace Company\ApplicationBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

class ApplicationBundle extends Bundle {
  public function registerExtensions(ContainerBuilder $container) {
    parent::registerExtensions($container);

    // register controllers
    $loader = new XmlFileLoader($container);
    $loader->load(__DIR__.'/Resources/config/controllers.xml');
  }
}

Замечание: више изложенная техника первоначально озвучена Kris Wallsmith в процессе совместной разработки проекта в OpenSky.

Теперь все готово. Вам нужно включить файл роутинга уровня бандла в конфигурацию роутинга уровня приложения, создайте Index директорию. Финальная структура директорий должна напоминать эту:

Company
| - ApplicationBundle
| | - Controller
| | | - IndexController.php
| | - Resources
| | | - config
| | | | - controller_routing.xml
| | | | - controllers.xml
| | | - views
| | | | - Index
| | | | | - index.php
| | - ApplicationBundle.php

После выполнения этих шагов, вы можете попробовать это в браузере, набрав URL:
your_application/your_front_controller.php/index
Tags:
Hubs:
+11
Comments 25
Comments Comments 25

Articles