Предлагаю вашему вниманию перевод вчерашнего поста одного из разработчиков Symfony2 о подходе к unit-тестированию контроллеров в Symfony2. Тема очень актуальна для Symfony2 разработчиков. Также стоит отметить, что в посте упоминается результат дискуссии на dev-groups об использовании контроллера в роли сервиса в Symfony2.
Даже имея большой опыт работы с MVC фреймворками, одна вещь постоянно остается нераскрытой — как тестировать контроллеры. Я думаю, что основная причина этому — неочевидность тестирования, так как контроллеры относятся к сорту элементов «черной магии» фреймворка. Существует множество соглашений по поводу размещения контроллеров в файловой системе, о каких зависимостях он должен знать, и какие должны быть у контроллера жесткие связи (слой view).
Такая обстановка вещей не предполагает легких путей тестирования контроллеров, пока вы не можете выделить контроллер и некоторые его основные зависимости для тестирование взаимодействия — вам необходимо запустить весь фреймворк и запустить функциональные тесты.
Так как этот процесс довольно сложный и комплексный, люди обычно не прибегают к unit-тестированию контроллеров, функциональные тесты — это максимум что вы можете получить, но обычно вообще тестирования не происходит.
Symfony2 полностью меняет дело.
Изначально в фреймворке Symfony2 есть только соглашению по загрузке контроллера. Экземпляр контроллера остается очень легковесным и не требует для своей работы расширения некоторого родительского класса. Если ваши контроллеры реализуют интерфейс ContainerAware, вы получите DIC (dependency injection container) внедренный через метод ContainerAware::setContainer(), который вы можете использовать для доступа к любому сервису, который вы объявили в DIC.
Рекомендуемый метод тестирования контроллеров некоторое время был приближением тестирования черного ящика, когда вы тестируете полные запросы к приложению и проверяете вывод приблизительно так:
Замечание: почитайте более о mock объектах в PHPUnit.
Хорошие новости состоят в том, что Symfony2 позволяет это. Теперь все ваши контроллеры могут выступать в роли сервисов. Прошлый, общепринятый вариант также поддерживается и незаменим для малых контроллеров, которые не требуют unit-тестирования.
Для того, чтобы контроллер из примера выше правильно взаимодействовал с Symfony2 и работал как следует, нам нужно следующее.
Создайте класс контроллера:
Создайте DIC конфигурацию, используя следующий xml:
Создайте конфигурацию роутинга:
Замечание: в примере выше, было использовано service_id:action вместо обычного BundleBundle:Controller:action (без суффикса ‘Action’).
Когда все это сделано, мы должны проинформировать Symfony2 о наших сервисах. Для того чтоб не произошло создание Dependency Injection расширения и создания точки конфигурационного файла, мы можем зарегистрировать наши сервисы напрямую:
Замечание: више изложенная техника первоначально озвучена Kris Wallsmith в процессе совместной разработки проекта в OpenSky.
Теперь все готово. Вам нужно включить файл роутинга уровня бандла в конфигурацию роутинга уровня приложения, создайте Index директорию. Финальная структура директорий должна напоминать эту:
Даже имея большой опыт работы с 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