php - Controladores verificables con dependencias
design-patterns dependencies (3)
¿Cómo puedo resolver las dependencias de un controlador que se puede probar?
Cómo funciona: un URI se enruta a un Controlador, un Controlador puede tener dependencias para realizar una determinada tarea.
<?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();
Esta es mi interpretación de los ejemplos de patrones de diseño.
Registro:
- Semifallo
- Acoplamiento apretado
- Comprobable? No
Localizador de servicios
- Comprobable? Difícil / No (?)
Pobre hombre di
- Comprobable
- Difícil de mantener con muchas dependencias.
Registro
<?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];
}
}
Localizador de servicios
<?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];
}
}
¿Cómo puedo resolver las dependencias de un controlador que se puede probar?
¿Cuáles serían las dependencias de las que está hablando en un controlador?
La mejor solución sería:
- Inyectando una fábrica de servicios en el controlador a través del constructor.
- Utilizando un contenedor DI para pasar los servicios específicos directamente.
Voy a tratar de describir ambos enfoques por separado en detalle.
Nota: todos los ejemplos omitirán la interacción con la vista, el manejo de la autorización, el manejo de las dependencias de la fábrica de servicios y otros detalles específicos
Inyección de fábrica.
La parte simplificada de la etapa bootstrap, que trata de lanzar cosas al controlador, se vería un poco así.
$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
}
Este enfoque proporciona una manera clara de extender y / o sustituir el código relacionado con la capa del modelo simplemente al pasar a una fábrica diferente como dependencia. En el controlador se vería algo así:
public function __construct( $factory )
{
$this->serviceFactory = $factory;
}
public function postLogin( $request )
{
$authentication = $this->serviceFactory->create( ''Authentication'' );
$authentication->login(
$request->getParameter(''username''),
$request->getParameter(''password'')
);
}
Esto significa que, para probar el método de este controlador, tendría que escribir una prueba de unidad, que simula el contenido de $this->serviceFactory
, la instancia creada y el valor pasado de $request
. Dicho simulacro tendría que devolver una instancia, que puede aceptar dos parámetros.
Nota: La respuesta al usuario debe ser manejada completamente por la instancia de vista, ya que la creación de la respuesta es parte de la lógica de la interfaz de usuario. Tenga en cuenta que el encabezado de ubicación HTTP también es una forma de respuesta.
La prueba de unidad para dicho controlador se vería como:
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
}
Se supone que los controladores son la parte más delgada de la aplicación. La responsabilidad del controlador es: tomar la entrada del usuario y, en función de esa entrada, modificar el estado de la capa del modelo (y en casos excepcionales, la vista actual) . Eso es.
Con contenedor DI
Este otro enfoque es ... bueno ... es básicamente un comercio de complejidad (restar en un lugar, agregar más en otros). También se transmite al tener contenedores de DI reales , en lugar de localizadores de servicio glorificados, como Pimple .
Mi recomendación: echa un vistazo a Auryn .
Lo que hace un contenedor DI es que, utilizando el archivo de configuración o la reflexión, determina las dependencias para la instancia que desea crear. Recoge dichas dependencias. Y pasa en el constructor por la instancia.
$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
}
Así que, aparte de la capacidad de lanzar excepciones, el arranque del controlador se mantiene prácticamente igual.
Además, en este punto ya debería reconocer que el cambio de un enfoque a otro requeriría sobre todo la reescritura completa del controlador (y las pruebas unitarias asociadas).
El método del controlador en este caso se vería algo así como:
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'')
);
}
En cuanto a escribir una prueba, en este caso, nuevamente, todo lo que necesita hacer es proporcionar algunos simulacros de aislamiento y simplemente verificar. Pero, en este caso, la prueba unitaria es más simple :
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
}
Como puedes ver, en este caso tienes una clase menos para burlarte.
Notas misceláneas
Acoplamiento al nombre (en los ejemplos - "autenticación"):
Como es posible que tenga avisos, en ambos ejemplos su código se acoplaría al nombre del servicio que se usó. E incluso si usas un contenedor DI basado en la configuración (como es posible en Symfony ), terminarás definiendo el nombre de la clase específica.
Los contenedores DI no son mágicos :
El uso de contenedores DI ha sido algo promocionado en los últimos dos años. No es una bala de plata. Incluso me atrevería a decir que: los contenedores DI son incompatibles con SOLID . Concretamente porque no funcionan con interfaces. Realmente no puede utilizar el comportamiento polimórfico en el código, que se inicializará con un contenedor DI.
Luego está el problema con la configuración basada en DI. Bueno ... es hermoso, mientras que el proyecto es pequeño. Pero a medida que el proyecto crece, el archivo de configuración también crece. Puede terminar con la gloriosa configuración de WALL of xml / yaml, que es entendida por una sola persona en el proyecto.
Y el tercer tema es la complejidad. Los buenos contenedores DI no son fáciles de hacer. Y si utiliza una herramienta de terceros, está introduciendo riesgos adicionales.
Demasiadas dependencias :
Si su clase tiene demasiadas dependencias, entonces no es una falla de DI como práctica. En cambio, es una clara indicación de que tu clase está haciendo demasiadas cosas. Está violando el principio de responsabilidad única .
Los controladores en realidad tienen (algunos) lógica :
Los ejemplos utilizados anteriormente eran extremadamente simples y en los que se interactuaba con la capa del modelo a través de un solo servicio. En el mundo real, sus métodos de control contendrán estructuras de control (bucles, condicionales, cosas).
El caso de uso más básico sería un controlador que maneje el formulario de contacto con el menú desplegable "Asunto". La mayoría de los mensajes se dirigirían a un servicio que se comunica con algunos CRM. Pero si el usuario elige "reportar un error", entonces el mensaje se debe pasar a un servicio de diferencia que automáticamente crea un ticket en el rastreador de errores y envía algunas notificaciones.
Es la unidad de PHP :
Los ejemplos de pruebas unitarias se escriben utilizando el marco PHPUnit . Si está utilizando algún otro marco, o está escribiendo pruebas manualmente, tendría que hacer algunas modificaciones básicas
Tendrás más pruebas :
El ejemplo de prueba de unidad no es el conjunto completo de pruebas que tendrá para el método de un controlador. Especialmente, cuando tienes controladores que no son triviales.
Otros materiales
Hay algunos ... emm ... temas tangenciales.
Apoyo para: la auto-promoción descarada
tratar con el control de acceso en arquitectura similar a MVC
Algunos marcos tienen el hábito desagradable de empujar las verificaciones de autorización (no confunda con "autenticación" ... tema diferente) en el controlador. Además de ser algo completamente estúpido, también introduce dependencias adicionales (a menudo, con alcance global) en los controladores.
Hay otra publicación que utiliza un enfoque similar para introducir el registro no invasivo
lista de conferencias
Está dirigido a las personas que desean aprender sobre MVC, pero en realidad hay materiales para educación general en OOP y prácticas de desarrollo. La idea es que, para cuando termines con esa lista, MVC y otras implementaciones de SoC solo harán que vayas "Oh, ¿esto tenía un nombre? Pensé que era solo sentido común".
implementando capa de modelo
Explica cuáles son esos "servicios" mágicos en la descripción anterior.
He intentado esto desde http://culttt.com/2013/07/15/how-to-structure-testable-controllers-in-laravel-4/
¿Cómo debes estructurar tus controladores para hacerlos verificables?
La prueba de sus controladores es un aspecto crítico de la construcción de una aplicación web sólida, pero es importante que solo pruebe las partes apropiadas de su aplicación.
Afortunadamente, Laravel 4 hace que la separación de las preocupaciones de su controlador sea realmente fácil. Esto hace que las pruebas de sus controladores sean realmente sencillas, siempre y cuando las haya estructurado correctamente.
¿Qué debo estar probando en mi controlador?
Antes de entrar en cómo estructurar sus controladores para comprobar su capacidad de prueba, primero es importante entender qué es exactamente lo que necesitamos probar.
Como mencioné en Configuración de su primer controlador Laravel 4, los controladores solo deben preocuparse por mover datos entre el modelo y la vista. No necesita verificar que la base de datos está obteniendo los datos correctos, solo que el Controlador está llamando al método correcto. Por lo tanto, sus pruebas de Controlador nunca deben tocar la base de datos.
Esto es realmente lo que voy a mostrarles hoy porque, de manera predeterminada, es bastante fácil deslizarse para unir el Controlador y el Modelo. Un ejemplo de mala práctica.
Para ilustrar lo que trato de evitar, aquí hay un ejemplo de un método de controlador:
public function index()
{
return User::all();
}
Esta es una mala práctica porque no tenemos manera de burlarnos de User::all();
y así la prueba asociada se verá obligada a golpear la base de datos.
Dependencia de la inyección al rescate.
Para solucionar este problema, tenemos que inyectar la dependencia en el Controlador. La inyección de dependencia es donde le pasas a la clase una instancia de un objeto, en lugar de dejar que ese objeto cree la instancia para sí mismo.
Al inyectar la dependencia en el Controlador, podemos pasar a la clase un simulacro en lugar de la base de datos en lugar del objeto de base de datos en sí durante nuestras pruebas. Esto significa que podemos probar la funcionalidad del controlador sin tocar la base de datos.
Como guía general, en cualquier lugar que vea una clase que está creando una instancia de otro objeto, generalmente es una señal de que esto podría manejarse mejor con la inyección de dependencia. Nunca quieres que tus objetos estén bien acoplados y, por lo tanto, al no permitir que una clase cree una instancia de otra clase, puedes evitar que esto suceda.
Resolución automática
Laravel 4 tiene una hermosa manera de manejar la inyección de dependencia. Esto significa que puede resolver clases sin ninguna configuración en muchos escenarios.
¡Esto significa que si pasa una clase una instancia de otra clase a través del constructor, Laravel automáticamente inyectará esa dependencia para usted!
Básicamente, todo funcionará sin ninguna configuración de su parte.
Inyectando la base de datos en un controlador
Ahora que entiendes el problema y la teoría de la solución, ahora podemos arreglar el Controlador para que no esté acoplado a la base de datos.
Si recuerdas la publicación de la semana pasada en los repositorios de Laravel, es posible que hayas notado que ya solucioné este problema.
Así que en lugar de hacer:
public function index()
{
return User::all();
}
Yo si:
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return $this->user->all();
}
Cuando se crea la clase UserController, el método __construct se ejecuta automáticamente. El método __construct se inyecta con una instancia del repositorio de usuarios, que luego se establece en la propiedad $ this-> user de la clase.
Ahora, cuando quiera usar la base de datos en sus métodos, puede usar la instancia de usuario $ this->.
Burlándose de la base de datos en sus pruebas de controlador
La verdadera magia sucede cuando vienes a escribir tus pruebas de controlador. Ahora que está pasando una instancia de la base de datos al Controlador, puede simular la base de datos en lugar de llegar a la base de datos. Esto no solo mejorará el rendimiento, sino que no tendrá ningún dato de prueba por ahí después de las pruebas.
Lo primero que haré es crear una nueva carpeta en el directorio de pruebas llamada funcional. Me gusta pensar que las pruebas del Controlador son pruebas funcionales porque estamos probando el tráfico entrante y la vista renderizada.
A continuación, crearé un archivo llamado UserControllerTest.php y escribiré el siguiente código de texto:
<?php
class UserControllerTest extends TestCase {
}
Burlándose de la burla
Si recuerdas volver a mi publicación, ¿Qué es el desarrollo guiado por pruebas ?, hablé de Mocks como un reemplazo de los objetos dependientes.
Para crear Mocks para las pruebas en Cribbb, voy a usar un fantástico paquete llamado Mockery.
La burla le permite simular objetos en su proyecto para que no tenga que usar la dependencia real. Al burlarse de un objeto, puede decirle a Mockery a qué método le gustaría llamar y qué le gustaría que le devolvieran.
Esto le permite aislar sus dependencias para que solo haga las llamadas requeridas del controlador para que la prueba pase.
Por ejemplo, si desea llamar al método all () en su objeto de base de datos, en lugar de golpear la base de datos, puede simular la llamada diciéndole a Mockery que desea llamar al método all () y debería devolver el valor esperado. No está probando si la base de datos puede devolver registros o no, solo le importa poder activar el método y manejar el valor de retorno.
Instalación de Mockery Como todos los buenos paquetes de PHP, Mockery se puede instalar a través de Composer.
Para instalar Mockery a través de Composer, agregue la siguiente línea a su archivo composer.json:
"require-dev": {
"mockery/mockery": "dev-master"
}
A continuación, instale el paquete:
composer install --dev
Configuración de la burla
Ahora para configurar Mockery, tenemos que crear un par de métodos de configuración en el archivo de prueba:
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;
}
El método setUp()
se ejecuta antes de cualquiera de las pruebas. Aquí estamos agarrando una copia del UserRepository
y creando un nuevo simulacro.
En el método mock()
, la $this->app->instance
le dice al contenedor IoC de Laravel que vincule la instancia $mock
a la clase UserRepository
. Esto significa que siempre que Laravel quiera usar esta clase, usará el simulacro en su lugar. Escribiendo tu primera prueba de controlador
A continuación puedes escribir tu primera prueba de controlador:
public function testIndex()
{
$this->mock->shouldReceive(''all'')->once();
$this->call(''GET'', ''user'');
$this->assertResponseOk();
}
En esta prueba, le pido al simulacro que llame al método all()
una vez en el UserRepository
. Luego llamo a la página utilizando una solicitud GET y luego afirmo que la respuesta fue correcta.
Conclusión
Los controladores de prueba no deben ser tan difíciles ni tan complicados como parece. Mientras se aíslen las dependencias y solo se prueben los bits correctos, los Controladores de prueba deben ser realmente sencillos.
Que esto te ayude.
La Programación Orientada a Aspectos puede ofrecerle una solución para los métodos de simulación incluso con el patrón de Localizador de Servicio. Busque el marco de pruebas AspectMock.
- Github: https://github.com/Codeception/AspectMock
- Video de Jeffrey Way: http://jeffrey-way.com/blog/2013/07/24/aspectmock-is-pretty-neat/