Контролируемые контроллеры с зависимостями

Как я могу разрешать зависимости с контроллером, который можно тестировать?

Как это работает: URI направляется на контроллер, контроллер может иметь зависимости для выполнения определенной задачи.

<?php require 'vendor/autoload.php'; /* * Registry * Singleton * Tight coupling * Testable? */ $request = new Example\Http\Request(); Example\Dependency\Registry::getInstance()->set('request', $request); $controller = new Example\Controller\RegistryController(); $controller->indexAction(); /* * Service Locator * * Testable? Hard! * */ $request = new Example\Http\Request(); $serviceLocator = new Example\Dependency\ServiceLocator(); $serviceLocator->set('request', $request); $controller = new Example\Controller\ServiceLocatorController($serviceLocator); $controller->indexAction(); /* * Poor Man * * Testable? Yes! * Pain in the ass to create with many dependencies, and how do we know specifically what dependencies a controller needs * during creation? * A solution is the Factory, but you would still need to manually add every dependencies a specific controller needs * etc. * */ $request = new Example\Http\Request(); $controller = new Example\Controller\PoorManController($request); $controller->indexAction(); 

Это моя интерпретация примеров шаблонов проектирования

Реестр:

  • одиночка
  • Тесная связь
  • Тестируемые? нет

Сервисный локатор

  • Тестируемые? Жесткий / Нет (?)

Бедный Человек Ди

  • Тестируемые
  • Трудно поддерживать со многими зависимостями

реестр

 <?php namespace Example\Dependency; class Registry { protected $items; public static function getInstance() { static $instance = null; if (null === $instance) { $instance = new static(); } return $instance; } public function set($name, $item) { $this->items[$name] = $item; } public function get($name) { return $this->items[$name]; } } 

Сервисный локатор

 <?php namespace Example\Dependency; class ServiceLocator { protected $items; public function set($name, $item) { $this->items[$name] = $item; } public function get($name) { return $this->items[$name]; } } 

Как я могу разрешать зависимости с контроллером, который можно тестировать?

Related of "Контролируемые контроллеры с зависимостями"

Какими будут зависимости, о которых вы говорите в контроллере?

Главным решением было бы:

  • впрыскивание фабрики сервисов в контроллер через конструктор
  • использование контейнера DI для передачи в конкретные услуги напрямую

Я попытаюсь подробно описать оба подхода отдельно.

Примечание: все примеры будут не учитывать взаимодействие с представлением, обработку авторизации, обработку зависимостей фабрики услуг и другие особенности

Инъекция завода

Упрощенная часть этапа начальной загрузки, которая связана с отправкой материала контроллеру, будет выглядеть как это

 $request = //... we do something to initialize and route this $resource = $request->getParameter('controller'); $command = $request->getMethod() . $request->getParameter('action'); $factory = new ServiceFactory; if ( class_exists( $resource ) ) { $controller = new $resource( $factory ); $controller->{$command}( $request ); } else { // do something, because requesting non-existing thing } 

Этот подход обеспечивает четкий способ расширения и / или замены кода, связанного с модельным слоем, просто путем перехода на другую фабрику в качестве зависимости. В контроллере он выглядит примерно так:

 public function __construct( $factory ) { $this->serviceFactory = $factory; } public function postLogin( $request ) { $authentication = $this->serviceFactory->create( 'Authentication' ); $authentication->login( $request->getParameter('username'), $request->getParameter('password') ); } 

Это означает, что для проверки этого метода контроллера вам нужно будет написать unit-test, который издевается над содержимым $this->serviceFactory , созданного экземпляра и переданного значения $request . Указанный макет должен будет вернуть экземпляр, который может принимать два параметра.

Примечание . Ответ на пользователя должен обрабатываться полностью экземпляром вида, поскольку создание ответа является частью логики пользовательского интерфейса. Имейте в виду, что заголовок HTTP-адреса также является формой ответа.

Единичный тест для такого контроллера будет выглядеть так:

 public function test_if_Posting_of_Login_Works() { // setting up mocks for the seam $service = $this->getMock( 'Services\Authentication', ['login']); $service->expects( $this->once() ) ->method( 'login' ) ->with( $this->equalTo('foo'), $this->equalTo('bar') ); $factory = $this->getMock( 'ServiceFactory', ['create']); $factory->expects( $this->once() ) ->method( 'create' ) ->with( $this->equalTo('Authentication')) ->will( $this->returnValue( $service ) ); $request = $this->getMock( 'Request', ['getParameter']); $request->expects( $this->exactly(2) ) ->method( 'getParameter' ) ->will( $this->onConsecutiveCalls( 'foo', 'bar' ) ); // test itself $instance = new SomeController( $factory ); $instance->postLogin( $request ); // done } 

Контроллеры должны быть самой тонкой частью приложения. Ответственность диспетчера заключается в следующем: принять пользовательский ввод и, основываясь на этом вводе, изменить состояние слоя модели (и в редком случае – текущий вид) . Вот и все.

С контейнером DI

Этот другой подход … ну .. это в основном торговля сложностью (вычесть в одном месте, добавить больше на другие). Он также передает информацию о наличии реальных контейнеров DI, а не прославленных сервисных локаторов, таких как Pimple .

Моя рекомендация: проверьте Аурин .

Что делает контейнер DI, используя либо файл конфигурации, либо отражение, он определяет зависимости для экземпляра, который вы хотите создать. Собирает указанные зависимости. И передается в конструкторе для экземпляра.

 $request = //... we do something to initialize and route this $resource = $request->getParameter('controller'); $command = $request->getMethod() . $request->getParameter('action'); $container = new DIContainer; try { $controller = $container->create( $resource ); $controller->{$command}( $request ); } catch ( FubarException $e ) { // do something, because requesting non-existing thing } 

Таким образом, помимо возможности генерировать исключение, загрузка контроллера остается практически такой же.

Кроме того, на этом этапе вы уже должны признать, что переход от одного подхода к другому будет в основном требовать полной перезаписи контроллера (и соответствующих модульных тестов).

Метод контроллера в этом случае будет выглядеть примерно так:

 private $authenticationService; #IMPORTANT: if you are using reflection-based DI container, #then the type-hinting would be MANDATORY public function __construct( Service\Authentication $authenticationService ) { $this->authenticationService = $authenticationService; } public function postLogin( $request ) { $this->authenticatioService->login( $request->getParameter('username'), $request->getParameter('password') ); } 

Что касается написания теста, в этом случае снова все, что вам нужно сделать, это предоставить некоторые макеты для изоляции и просто проверить. Но в этом случае модульное тестирование проще :

 public function test_if_Posting_of_Login_Works() { // setting up mocks for the seam $service = $this->getMock( 'Services\Authentication', ['login']); $service->expects( $this->once() ) ->method( 'login' ) ->with( $this->equalTo('foo'), $this->equalTo('bar') ); $request = $this->getMock( 'Request', ['getParameter']); $request->expects( $this->exactly(2) ) ->method( 'getParameter' ) ->will( $this->onConsecutiveCalls( 'foo', 'bar' ) ); // test itself $instance = new SomeController( $service ); $instance->postLogin( $request ); // done } 

Как вы можете видеть, в этом случае у вас есть еще один класс, чтобы высмеивать.

Разное

  • Связь с именем (в примерах – «аутентификация»):

    Как вы могли бы заметить, в обоих примерах ваш код будет связан с именем службы, которое было использовано. И даже если вы используете конфигурационный контейнер DI (как это возможно в symfony ), вы все равно определите имя конкретного класса.

  • Контейнеры DI не являются волшебными :

    Использование контейнеров DI было несколько раздуто в последние пару лет. Это не серебряная пуля. Я бы даже сказал, что: контейнеры DI несовместимы с SOLID . В частности, потому что они не работают с интерфейсами. Вы действительно не можете использовать полиморфное поведение в коде, которое будет инициализировано контейнером DI.

    Тогда возникает проблема с DI на основе конфигурации. Ну .. это просто красиво, а проект крошечный. Но по мере роста проекта файл конфигурации также растет. В итоге вы можете получить великолепную WALL конфигурации xml / yaml, которая понимается только одним человеком в проекте.

    И третий вопрос – сложность. Хорошие контейнеры DI не просты в изготовлении. И если вы используете сторонний инструмент, вы вводите дополнительные риски.

  • Слишком много зависимостей :

    Если у вашего класса слишком много зависимостей, то это не отказ DI как практики. Вместо этого это явное указание , что ваш класс делает слишком много вещей. Это нарушает принцип единой ответственности .

  • Контроллеры фактически имеют (некоторую) логику :

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

    Основным вариантом использования будет контроллер, который обрабатывает контактную форму с раскрывающимся списком «субъект». Большинство сообщений будут направлены на службу, которая связывается с некоторым CRM. Но если пользователь выбирает «сообщить об ошибке», тогда сообщение должно быть передано службе разницы, которая автоматически создает билет в трекере ошибок и отправляет некоторые уведомления.

  • Это модуль PHP :

    Примеры модульных тестов написаны с использованием фреймворка PHPUnit . Если вы используете какую-либо другую структуру или вручную пишете тесты, вам придется внести некоторые основные изменения

  • У вас будет больше тестов :

    Пример unit-test – это не весь набор тестов, которые вы будете иметь для метода контроллера. Особенно, когда у вас есть контроллеры, которые нетривиальны.

Другие материалы

Есть некоторые .. эмм … тангенциальные предметы.

Скобки для: бесстыдной саморекламы

  • управление доступом в MVC-подобной архитектуре

    Некоторые структуры имеют неприятную привычку подталкивать проверки авторизации (не путайте с «аутентификацией» .. другой вопрос) в контроллере. Помимо того, что он абсолютно глупый, он также вводит в контроллеры дополнительные зависимости (часто – глобально).

    Существует еще одно сообщение, в котором используется аналогичный подход для введения неинвазивного каротажа

  • список лекций

    Это направлено на людей, которые хотят узнать о MVC, но там есть материалы для общего образования в ООП и практики развития. Идея заключается в том, что к тому моменту, когда вы закончите с этим списком, MVC и другие реализации SoC приведут к тому, что вы пойдете «О, у этого было имя? Я думал, что это просто здравый смысл».

  • реализация модельного слоя

    Объясняет, что эти магические «услуги» находятся в описании выше.

Я пробовал это с http://culttt.com/2013/07/15/how-to-structure-testable-controllers-in-laravel-4/

Как вы должны структурировать контроллеры, чтобы сделать их проверяемыми.?

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

К счастью, Laravel 4 очень легко отделяет проблемы вашего контроллера. Это делает тестирование ваших контроллеров действительно прямым, если вы правильно структурировали их.

Что я должен тестировать в своем контроллере?

Прежде чем я расскажу о том, как структурировать контроллеры для проверки, сначала важно понять, что именно нам нужно проверить.

Как я уже говорил в разделе «Настройка вашего первого контроллера Laravel 4», контроллеры должны учитывать только перемещение данных между моделью и представлением. Вам не нужно проверять, что база данных вытаскивает правильные данные, только то, что контроллер вызывает правильный метод. Поэтому ваши тесты контроллера никогда не должны касаться базы данных.

Это действительно то, что я собираюсь показать вам сегодня, потому что по умолчанию довольно легко просочиться в сочетании с контроллером и моделью вместе. Пример плохой практики

Для иллюстрации того, что я пытаюсь избежать, приведен пример метода контроллера:

 public function index() { return User::all(); } 

Это плохая практика, потому что у нас нет способа насмехаться с User::all(); и поэтому связанный тест будет вынужден попасть в базу данных.

Зависимость Инъекция к спасению

Чтобы обойти эту проблему, нам нужно ввести зависимость в контроллер. Dependency Injection – это то, где вы передаете классу экземпляр объекта, а не позволяете этому объекту создавать экземпляр для себя.

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

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

Автоматическое разрешение

Laravel 4 имеет прекрасный способ обработки инъекций. Это означает, что вы можете разрешать классы без какой-либо конфигурации вообще во многих сценариях.

Это означает, что если вы передадите классу экземпляр другого класса через конструктор, Laravel автоматически добавит эту зависимость для вас!

В принципе, все будет работать без какой-либо конфигурации с вашей стороны.

Внедрение базы данных в контроллер

Итак, теперь вы понимаете проблему и теорию решения, теперь мы можем исправить контроллер, поэтому он не связан с базой данных.

Если вы помните, что на прошлой неделе на Laravel Repositories вы могли заметить, что я уже исправил эту проблему.

Поэтому вместо того, чтобы делать:

 public function index() { return User::all(); } 

Я сделал:

 public function __construct(User $user) { $this->user = $user; } /** * Display a listing of the resource. * * @return Response */ public function index() { return $this->user->all(); } 

Когда создается класс UserController, автоматически запускается метод __construct. В метод __construct вводится экземпляр репозитория User, который затем устанавливается в свойстве $ this-> user этого класса.

Теперь, когда вы хотите использовать базу данных в своих методах, вы можете использовать экземпляр $ this-> user.

Издевательствование базы данных в тестах Контроллера

Настоящая магия случается, когда вы приходите писать тесты Контроллера. Теперь, когда вы передаете экземпляр базы данных контроллеру, вы можете издеваться над базой данных, а не просто ударять по базе данных. Это не только улучшит производительность, но и после ваших тестов у вас не будет никаких тестовых данных.

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

Затем я создам файл UserControllerTest.php и напишу следующий шаблонный код:

 <?php class UserControllerTest extends TestCase { } 

Издевательская над нами

Если вы вспомните мой пост, что такое Test Driven Development ?, я говорил о Mocks как о замене для зависимых объектов.

Чтобы создать Mocks для тестов в Cribbb, я собираюсь использовать фантастический пакет под названием Mockery.

Mockery позволяет вам издеваться над объектами в вашем проекте, поэтому вам не нужно использовать реальную зависимость. Издеваясь над объектом, вы можете сказать Mockery, какой метод вы хотите вызвать и что вы хотите вернуть.

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

Например, если вы хотите вызвать метод all () в объекте базы данных, вместо фактического попадания в базу данных вы можете высмеять вызов, указав Mockery, который вы хотите вызвать метод all (), и он должен вернуть ожидаемое значение. Вы не проверяете, может ли база данных возвращать записи или нет, вам нужно только запустить метод и обработать возвращаемое значение.

Установка Mockery Как и все хорошие пакеты PHP, Mockery можно установить через Composer.

Чтобы установить Mockery через Composer, добавьте следующую строку в ваш файл composer.json:

 "require-dev": { "mockery/mockery": "dev-master" } 

Затем установите пакет:

 composer install --dev 

Настройка издевательства

Теперь, чтобы настроить Mockery, нам нужно создать пару тестовых файлов:

 public function setUp() { parent::setUp(); $this->mock = $this->mock('Cribbb\Storage\User\UserRepository'); } public function mock($class) { $mock = Mockery::mock($class); $this->app->instance($class, $mock); return $mock; } 

Метод setUp() запускается до любого из тестов. Здесь мы собираем копию UserRepository и создаем новый макет.

В методе mock() $this->app->instance сообщает контейнеру IoC Laravel о $this->app->instance $mock к классу UserRepository . Это означает, что всякий раз, когда Laravel хочет использовать этот класс, он будет использовать макет вместо этого. Написание первого теста контроллера

Затем вы можете написать свой первый тест контроллера:

 public function testIndex() { $this->mock->shouldReceive('all')->once(); $this->call('GET', 'user'); $this->assertResponseOk(); } 

В этом тесте я прошу макету вызывать метод all() один раз в UserRepository . Затем я вызываю страницу с помощью запроса GET, а затем утверждаю, что ответ был в порядке.

Вывод

Контрольные контроллеры не должны быть такими сложными или сложными, как это делается. Пока вы изолируете зависимости и проверяете только правильные биты, тестирование контроллеров должно быть действительно прямым.

может это вам помочь.

Аспектно-ориентированное программирование может дать ваше решение для издевательских методов даже с шаблоном Locator. Найдите структуру тестирования AspectMock.

  1. Github: https://github.com/Codeception/AspectMock
  2. Видео от Джеффри Уэй: http://jeffrey-way.com/blog/2013/07/24/aspectmock-is-pretty-neat/