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

Предлагаю вашему вниманию перевод вчерашнего поста одного из разработчиков 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
+11
1 октября 2010, 17:35
24
skorney 16,0

комментарии (25)

0
VolCh #
Интересно, надо будет попробовать, а то я из числа тех, кто «обычно не прибегают к unit-тестированию контроллеров, обычно вообще тестирования не происходит». Модельки погонять (да и то без репозиториев) — это святое, а вот контроллеры…

Вот только не понял, вводим переменную templating, инициализируем её mock'ом, запускаем тест, происходит вызов в action, всё ок. А вот как эта переменная получит значение (и какое) при вызове обычным для симфони путём?
+2
m_z #
статья отвратительна.
Человек не понимает разницу между unit-тестированием и функциональным (приемочным) тестировании.
Он выдумал три недостатка приемочного тестирования. Контраргументы:
1. Конечно нужно запустить все ядро, ведь в этом и суть приемочного тестирование — тестируется работа всей системы в целом.
2. Конечно чувствительны. тесты пишутся с учетом спецификации, если эти изменения в дизайне не прописаны в спецификации — тесты должны это показать
3. Долго, но никто их и не запускает каждые 5 минут, а только, например, перед релизом.

Далее он предлагает использовать магический трюк в симфони чтобы сделать лишь что? протестировать внутренности метода контроллера! но зачем?? Это useless tests.
0
pilot #
вообще речь не идет о том зачем, это для себя решает каждый сам в процессе реализаций той или иной задаче, в посте показано КАК, как это можно сделать в symfony2
0
m_z #
возможно кто-то видит в этом смысле (я жду пока кто-нибудь объяснит его). я лишь не понимаю зачем говорить что «это» является заменой приемочных тестов, которые, по словам авторам, медленные и «вчерашний день».
0
pilot #
думаю, что эти слова --«это» является заменой приемочных тестов, которые, по словам авторам, медленные и «вчерашний день»-- вряд ли авторства skorney
0
VolCh #
Смысл — быстро (без запуска всего ядра) и без зависимости от дизайна (вернее представления) и других связей протестировать логику контроллера, чтобы потом, когда на тех же условиях сфейлит функциональный (он же интеграционный) тест, быть уверенным, что уж в контроллере-то у нас всё ок.
И где вы нашли, что «это» является заменой функциональных тестов? Это вариант простой (относительно)реализации модульных тестов. Теперь перед запуском функциональных я буду проводить модульные и на котроллерах, потому что раньше не знал, как изолировать контроллер от представления в symfony.
+3
VolCh #
>Далее он предлагает использовать магический трюк в симфони чтобы сделать лишь что? протестировать внутренности метода контроллера! но зачем?? Это useless tests.

Как зачем? А если там ошибка? :-/
0
m_z #
какая ошибка?
+3
AmdY #
как-то три слепых мудреца пошли посмотреть на чудо-юдо невиданное, на слона. пришли и давай расспрашивать что же он из себя представляет. первый узнал что он серый, второй узнал что у него большие уши, третий узнал что у него маленький хвост. и ушли огорчённые мудрецы, подумав что слон тоже самое что и заяц.
0
VolCh #
Даже в этом примитивном случае может быть, что, например, контроллер рендерит другой шаблон, рендерит нужный, но несколько раз, или вообще его не рендерит.
0
korchasa #
Для этого есть приемочные тесты, в которых не надо зорко бдить за соответствие моков остальному коду. Моки вообще зло, они дают ложную уверенность отсутствия багов.
+1
VolCh #
Приёмочные тесты, имхо, своим названием намекают, что они плохо годятся для запуска каждые 5 минут, они хороши именно для сдачи/приёмки, но несут мало полезной для отладки информации в случае фейла. А ложную уверенность дают любые выполняющиеся тесты ;)
0
korchasa #
Юнит тесты с моками в этом плане только хуже. Приемочный тест даст вам сигнал, что ошибка есть. Если вы забыли синхронизировать протокол мока с живым объектом, то о ошибке вы даже не узнаете. Про тестирование контроллеров вообще, я склоняюсь к мнению, что они быть настолько простыми, чтобы их не надо было тестировать.
0
VasilioRuzanni #
"… протестировать внутренности метода контроллера… ...useless tests."

Как это так useless???
+2
m_z #
автор показывает testIndexAction, в этом тесте есть мок (который показывает как работает indexAction внутри. И кстати, мелочь, но уже в таком простом примере конфигурация мока заняла 5 строк, в сложных проектах, когда в шаблоны передаются переменные, когда один метод возвращает разные форматы и т.п., этот мок займет экран). И завершается этот метод $this->assertEquals('success', $controller->indexAction()); который максимум протестировал что мок был сконфигрурирован правильно. И вы можете с некоторой уверенностью сказать что ваш indexAction работает без «ошибок»? Я — нет.
0
VolCh #
Этот тест демонстрирует, что шаблон вызывается с нужными параметрами, а не что мок был правильно сконфигурирован. Другой тест (для другого контроллера) покажет, что если id какой-то сущности, переданный контроллеру как параметр, есть в хранилище, то шаблону эта сущность передастся. Третий тест покажет, что если сущности с этим id нет в хранилище, то вместо вызова шаблона выбрасывается исключение HTTPNotFound…

Статья не о том, что функциональное тестирование не нужно, а о том как проводить модульное тестирование контроллеров symfony2. Или может вы считаете, что unit tests в принципе useless?

+1
m_z #
если разбирать конкретно этот пример, то вопрос. кто определяет список «нужных параметров»? я добавлю в шаблоне вызов неизвестной переменной. Сайт поломается, а тесты скажут всё OK. И наоборот, удалив переменную из шаблонов, тесты не сообщат о неиспользованной переменной. Т.е. все, о чем они будут сообщать нам, это о том, когда мы удалили/добавили переменные непосредственно в методе. Вот поэтому я считаю что useless.
Да, хранилище это тоже сервис, как и реквест, респонс, формы. Уже представляете сколько уйдет времени чтобы написать нужные моки?
Откуда я взял что автор считает его способ заменой функциональных тестов? Зачем тогда начинать статью про то как ужасны функциональные тесты и в идеальном мире ..?
Выше, про то, что такое ошибка я спросил не просто так. «контроллер рендерит другой шаблон, рендерит нужный, но несколько раз, или вообще его не рендерит. » — все это покажут функциональные тесты. testIndexAction is useless again.

PS: Symfony2 мне очень нравится и хочется чтобы фреймворк оставался таким же простым как сейчас, без этих «всё — сервисы». И нет, я считаю что unit tests конкретно для контроллеров useless.
0
VolCh #
Юнит-тесты контроллера и не должны показывать ошибки в шаблоне, шаблон это другой юнит, а вот если вы забудете вызвать шаблон, то тесты покажут ошибку. То, что в шаблоне есть неназначенная в контроллере переменная как раз зона ответственности интеграционных тестов, а юнит тесты и не должны показывать такие ошибки. С хранилищем проще, по-моему, использовать простенькое хранилище в памяти, реализующее нужные интерфейсы (стаб, а не мок).

Он, имхо, считает этот способ хорошей заменой использованию функциональных тестов приложения в качестве юнит-тестов контроллера (или вообще их нетестированию), что не стоит стрелять из пушки по воробьям и не стоит вообще не стрелять только лишь из-за желания экономить заряды пушки, если есть «мелкашка». Зачем стрелять по воробьям, по каждому ли или же только по особо упитанным каждый решает сам, в статье лишь описание процесса стрельбы. Представьте себё её название в виде «Как стрелять по воробьям из мелкашки», а начало как «Стрелять по воробьям из пушки неэффективно, лучше взять мелкашку» (а ваше useless как «да зачем по ним вообще стрелять, у нас есть пушка, давайте стрелять по слонам» :) )

А функциональные тесты не покажут, что шаблон вызывался дважды, если данные первого (или второго) вызова не будут возвращены в ответе. Также они не покажут, что данные действительно рендерятся (происходит вызов шаблонизатора), а не захардкодены в контролере. Ну и не покажут, что вызывался другой шаблон, если в нём тоже есть '/My Cool Website/'. Надо это показывать или нет, если ответ всё равно ожидаемый — отдельный холивар :)
0
develop7 #
Человек не понимает разницу между unit-тестированием и функциональным (приемочным) тестировании.
да плевать. речь не об этом вообще-то
–1
skorney #
Уверяю вас, автор хорошо понимает разницу unit-тестирования и функционального, но не в этом суть.
А вы думаете что все контроллеры одинаковые? и что их тестировать не нужно? Топик написал разработчик фреймворка — для разработчиков это очень нужно.

+1
krestjaninoff #
Термин POPO автор придумал сам? Просто если есть у нас есть Plain Old PHP Objects, то видимо должны быть и Enterprice PHP Beans? :)
0
korchasa #
В контроллерах не должно быть такого количества когда, чтобы его захотелось протестировать модульными тестами. Хочешь юнит-тест? Пиши сервис, стратегию, etc.
0
kpower #
Объясните мне, плз, зачем нужно тестировать контроллеры?
Нет, я понимаю, тестировать модель — поменял в одном месте, проверил в другом, а ошибка вылезла в третьем (о котором забыл) — такое действительно хотелось бы отсекать.
А с контроллером что? Поменял его — и ничего, кроме его собственной работы, не поменялось. Не проще ли в таком случае тупо зайти на конкретную страницу/ы и проверить — все ли так (ведь все равно будем это делать, чтобы хотя бы внешний вид оглядеть). Нет, я не спорю, если это можно было бы сделать автоматом — было бы благо. Однако, когда мы можем лишь проверить, запустилось ли в принципе (по-сути, это все) — получается, что мы больше кода пишем для проверки…
0
skorney #
Я может тут буду не совсем прав, но когда переводил, думал так. Я вроде понял о чем вы — контроллеры отвечают за отклик на конкретные параметры, они не так важны как модели, можно не тестировать. В общем да. НО! Вы смотрите как пользователь фреймворка. А статья от контрибутора кода в фреймворк, находящийся в стадии Preview Release. Когда вы вносите любые правки в код фреймворка — вы должны добиться прохождения всех unit-тестов. Я думаю автор просто хотел добавить тестов еще и на механизм контроллеров (который входит в состав фреймворка). Просто чтобы сделать будущие правки фреймворка более безопасными. Я думаю просто поднимается тема — почему не тестировать? — давайте и контроллеры тестировать. Еще этим переводом я хотел подчеркнуть, что в разработке на sf2 очень важно придерживаться TDD- так как фреймворк находится в активной стадии разработки.
0
kpower #
Аааа! Ну, тогда ясно. Спасибо. С таким объяснением согласен.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.