Advertisement
  1. Code
  2. PHP

Prueba de los controladores Laravel

Scroll to top
Read Time: 23 min

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

Prueba de controladores no es la cosa más fácil en el mundo. Bueno, déjame reformular eso: probarlos es muy facil; Lo que es difícil, al menos al principio, es determinar qué probar.

Should a controller test verify text on the page? ¿Debo tocar la base de datos? ¿Debe asegurarse de que existen variables en la vista? Si este es su primer paseo de heno, estas cosas pueden ser confusas! Déjame ayudar.

Las pruebas del controlador deben verificar las respuestas, asegurarse de que se activan los métodos correctos de acceso a la base de datos y afirmar que las variables de instancia apropiadas se envían a la vista.

El proceso de prueba de un controlador se puede dividir en tres partes.

  • Aislar: Mock todas las dependencias (tal vez excluyendo  View).
  • Llamar: Disparar el método del controlador deseado.
  • Asegúrarse: Realizar afirmaciones, verificando que la etapa se ha ajustado correctamente.

El Hello World de la prueba del Controlador

La mejor manera de aprender estas cosas es a través de ejemplos. Aquí está el "hello world" de las pruebas de controladores en Laravel.

1
2
<?php
3
4
# app/tests/controllers/PostsControllerTest.php

5
6
class PostsControllerTest extends TestCase {
7
8
  public function testIndex()
9
  {
10
      $this->client->request('GET', 'posts');
11
  }
12
13
}

Laravel aprovecha un puñado de componentes Symfony para facilitar el proceso de prueba de rutas y vistas, incluyendo HttpKernel, DomCrawler y BrowserKit. Esta es la razón por la cual es primordial que sus pruebas PHPUnit heredan, no PHPUnit\_Framework\_TestCase, pero TestCase. No te preocupes, Laravel todavía extiende la primera, pero ayuda a configurar la aplicación de Laravel para la prueba, así como proporciona una variedad de métodos de aserción de ayuda que te animan a usar. Más sobre eso en breve.

En el fragmento de código anterior, hacemos una solicitud GET a /post o localhost:8000/posts. Suponiendo que esta línea se agregue a una nueva instalación de Laravel, Symfony lanzará una NotFoundHttpException. Si está trabajando, intente ejecutar phpunit desde la línea de comandos.

1
2
$ phpunit
3
1) PostsControllerTest::testIndex
4
Symfony\Component\HttpKernel\Exception\NotFoundHttpException:

En la palabra humana, esto se traduce básicamente a, "Hey, traté de llamar a esa ruta, pero usted no tiene nada registrado, ¡tonto!"

Como se puede imaginar, este tipo de solicitud es bastante común al punto de que tiene sentido proporcionar un método auxiliar, como $this->call(). De hecho, ¡Laravel hace eso mismo! Esto significa que el ejemplo anterior puede ser refactorizado, así:

1
2
#app/tests/controllers/PostsControllerTest.php

3
4
public function testIndex()
5
{
6
    $this->call('GET', 'posts');
7
}

La sobrecarga es su amigo

Aunque seguiremos con la funcionalidad básica en este capítulo, en mis proyectos personales, voy más allá al permitir métodos como $this->get(), $this->post(), etc. Gracias a PHP sobrecarga, esto sólo requiere la adición de un único método, que se podría añadir a app/tests/TestCase.php.

1
2
# app/tests/TestCase.php

3
4
public function __call($method, $args)
5
{
6
    if (in_array($method, ['get', 'post', 'put', 'patch', 'delete']))
7
    {
8
        return $this->call($method, $args[0]);
9
    }
10
11
    throw new BadMethodCallException;
12
}

Ahora, eres libre de escribir $this->get('posts') y lograr el mismo resultado que los dos ejemplos anteriores. Como se señaló anteriormente, sin embargo, vamos a seguir con la funcionalidad base del marco para la simplicidad.

Para hacer pasar la prueba, sólo tenemos que preparar la ruta adecuada.

1
2
<?php
3
4
# app/routes.php

5
6
Route::get('posts', function()
7
{
8
    return 'all posts';
9
});

Ejecutar phpunit de nuevo nos devolverá verde.


Assertions de Laravel Helper

Una prueba que te encontrarás escribiendo repetidamente es aquel que asegura que un controlador pasa una variable particular a una vista. Por ejemplo, el método de index de PostsController debe pasar una variable $posts a su vista asociada, ¿verdad? De esta forma, la vista puede filtrar todos los mensajes y mostrarlos en la página. Esta es una prueba importante para escribir!

Si es que una tarea común, entonces, una vez más, ¿no tendría sentido para Laravel para proporcionar una afirmación de ayuda para lograr esto mismo? Por supuesto. ¡Y, por supuesto, a Laravel tambien!

Illuminate\Foundation\Testing\TestCase incluye una serie de métodos que reducirán drásticamente la cantidad de código necesaria para realizar aserciones básicas. Esta lista incluye:

  • assertViewHas
  • assertResponseOk
  • assertRedirectedTo
  • assertRedirectedToRoute
  • assertRedirectedToAction
  • assertSessionHas
  • assertSessionHasErrors

Los siguientes ejemplos llaman a GET /posts y verifica que sus vistas reciben la variable, $posts.

1
2
# app/tests/controllers/PostsControllerTest.php

3
4
public function testIndex()
5
{
6
    $this->call('GET', 'posts');
7
8
    $this->assertViewHas('posts');
9
}

Sugerencia: Cuando se trata de formatear, prefiero proporcionar un salto de línea entre la afirmación de una prueba y el código que prepara la etapa.

AssertViewHas es simplemente un poco de azúcar que inspecciona el objeto de respuesta - que se devuelve desde  $this->call() - y verifica que los datos asociados con la vista contienen una variable posts.

Al inspeccionar el objeto de respuesta, tiene dos opciones principales.

  • $response->getOriginalContent(): busca el contenido original o la devuelta View. Opcionalmente, puede acceder a la propiedad original directamente, en lugar de llamar al método getOriginalContent.
  • $Response->getContent(): Obtiene la salida renderizada. Si se devuelve una instancia View de la ruta, entonces getContent() será igual a la salida HTML. Esto puede ser útil para verificaciones de DOM, como "la vista debe contener esta cadena".

Supongamos que la ruta de posts consta de:

1
2
<?php
3
4
# app/routes.php

5
6
Route::get('posts', function()
7
{
8
    return View::make('posts.index');
9
});

Deberíamos ejecutar phpunit, se fallara con un útil paso siguiente mensaje:

1
2
1) PostsControllerTest::testIndex
3
Failed asserting that an array has the key 'posts'.

Para hacerlo verde, simplemente buscamos los posts y lo pasamos a la vista.

1
2
3
# app/routes.php

4
5
Route::get('posts', function()
6
{
7
    $posts = Post::all();
8
9
    return View::make('posts.index', ['posts', $posts]);
10
});

Una cosa a tener en cuenta es que, como el código actual, sólo asegura que la variable, $posts, se pasa a la vista. No inspecciona su valor. El assertViewHas opcionalmente acepta un segundo argumento para verificar el valor de la variable, así como su existencia.

1
2
3
# app/tests/controllers/PostsControllerTest.php

4
5
public function testIndex()
6
{
7
    $this->call('GET', 'posts');
8
9
    $this->assertViewHas('posts', 'foo');
10
}

Con este código modificado, a menos que la vista tenga una variable, $posts, que sea igual a foo, la prueba fallará. En esta situación, sin embargo, es probable que prefieramos no especificar un valor, sino que declaramos que el valor es una instancia de la clase Illuminate\Database\Eloquent\Collection de Laravel. ¿Cómo podríamos lograr eso? PHPUnit proporciona una afirmación assertInstanceOf útil para llenar esta misma necesidad!

1
2
3
# app/tests/controllers/PostsControllerTest.php

4
5
public function testIndex()
6
{
7
    $response = $this->call('GET', 'posts');
8
9
    $this->assertViewHas('posts');
10
11
    // getData() returns all vars attached to the response.

12
    $posts = $response->original->getData()['posts'];
13
14
    $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $posts);
15
}

Con esta modificación, hemos declarado que el controlador debe pasar $posts - una instancia de Illuminate\Database\ Eloquent\Collection - a la vista. Excelente.


Mocking la base de datos

Hay un problema flagrante con nuestras pruebas hasta ahora. ¿Lo atrapaste?

Para cada prueba, se está ejecutando una consulta SQL en la base de datos. Aunque esto es útil para ciertos tipos de pruebas (aceptación, integración), para las pruebas básicas del controlador, sólo servirá para disminuir el rendimiento.

He perforado esto en su cráneo varias veces en este momento. No estamos interesados en probar la capacidad de Eloquent de buscar registros de una base de datos. Tiene sus propias pruebas. ¡Taylor sabe que funciona! No perdamos tiempo y poder de procesamiento repitiendo las mismas pruebas.

En su lugar, lo mejor es burlarse de la base de datos, y simplemente verificar que los métodos apropiados se llaman con los argumentos correctos. O, en otras palabras, queremos asegurarnos de que Post::all() nunca se dispara y llega a la base de datos. Sabemos que funciona, por lo que no requiere pruebas.

Esta sección dependerá en gran medida de la biblioteca Mockery. Por favor revise ese capítulo de mi libro, si aún no está familiarizado con él.

Refactorización Requerida

Lamentablemente, hasta ahora, hemos estructurado el código de una manera que hace prácticamente imposible probar.

1
2
# app/routes.php

3
4
Route::get('posts', function()
5
{
6
    // Ouch. We can't test this!!

7
    $posts = Post::all();
8
9
    return View::make('posts.index')
10
        ->with('posts', $posts);
11
});

Esta es precisamente la razón por la que se considera mala práctica anidar llamadas de Eloquent en sus controladores. No confundas las fachadas de Laravel, que son comprobables y pueden intercambiarse con mocks (Queue::shouldReceive()), con tus modelos Eloquent. La solución es inyectar la capa de base de datos en el controlador a través del constructor. Esto requiere una cierta refactorización.

Advertencia: almacenar la lógica dentro de las devoluciones de llamada de ruta es útil para proyectos pequeños y API, pero hacen que las pruebas sean increíblemente difíciles. Para aplicaciones de cualquier tamaño considerable, utilice controladores.

Registremos un nuevo recurso reemplazando la ruta de posts con:

1
2
# app/routes.php

3
4
Route::resource('posts', 'PostsController');

... y crear el controlador de recursos necesarios con Artisan.

1
2
$ php artisan controller:make PostsController
3
Controller created successfully!

Ahora, en lugar de referenciar directamente el modelo Post, lo inyectaremos en el constructor del controlador. He aquí un ejemplo condensado que omite todos los métodos de restful, excepto el que estamos actualmente interesados en las pruebas.

1
2
<?php
3
4
# app/controllers/PostsController.php

5
6
class PostsController extends BaseController {
7
8
  protected $post;
9
10
  public function __construct(Post $post)
11
  {
12
      $this->post = $post;
13
  }
14
15
  public function index()
16
  {
17
      $posts = $this->post->all();
18
19
      return View::make('posts.index')
20
          ->with('posts', $posts);
21
  }
22
23
}

Tenga en cuenta que es una mejor idea escribir la sugerencia de una interfaz, en lugar de hacer referencia al modelo Eloquent, en sí. Pero, ¡una cosa a la vez! Vamos a trabajar hasta eso.

Esta es una manera significativamente mejor de estructurar el código. Debido a que el modelo ahora se inyecta, tenemos la capacidad de intercambiarlo con una versión simulada para la prueba. He aquí un ejemplo de hacer eso:

1
2
<?php
3
4
# app/tests/controllers/PostsControllerTest.php

5
6
class PostsControllerTest extends TestCase {
7
8
  public function __construct()
9
  {
10
      // We have no interest in testing Eloquent

11
      $this->mock = Mockery::mock('Eloquent', 'Post');
12
  }
13
14
  public function tearDown()
15
  {
16
      Mockery::close();
17
  }
18
19
  public function testIndex()
20
  {
21
      $this->mock
22
           ->shouldReceive('all')
23
           ->once()
24
           ->andReturn('foo');
25
26
      $this->app->instance('Post', $this->mock);
27
28
      $this->call('GET', 'posts');
29
30
      $this->assertViewHas('posts');
31
  }
32
33
}

El principal beneficio de esta reestructuración es que, ahora, la base de datos nunca será usada innecesariamente. En su lugar, usando Mockery, simplemente verificamos que el método all se dispara en el modelo.

1
2
$this->mock
3
    ->shouldReceive('all')
4
    ->once();

Por desgracia, si elige renunciar a la codificación a una interfaz, y en su lugar inyectar el modelo Post en el controlador, un poco de trucos tiene que ser utilizados con el fin de evitar el uso de Eloquent de la estática, que puede chocar con Mockery. Esta es la razón por la que hijack tanto el Post y Eloquent clases dentro del constructor de la prueba, antes de las versiones oficiales se han cargado. De esta manera, tenemos una lista limpia para declarar cualquier expectativa. La desventaja, por supuesto, es que no podemos usar ningún método existente, a través del uso de métodos de Mockery, como makePartial().

El contenedor de IoC

El contenedor IoC de Laravel facilita drásticamente el proceso de inyección de dependencias en sus clases. Cada vez que se solicita un controlador, se resuelve fuera del contenedor de IoC. Como tal, cuando necesitamos declarar que una versión simulada de Post se debe utilizar para la prueba, sólo tenemos que proporcionar a Laravel la instancia de Post que debe utilizarse.

1
2
$this->app->instance('Post', $this->mock);

Piense en este código como diciendo: "Hey Laravel, cuando necesitas una instancia de Post, quiero que uses mi versión mock." Debido a que la aplicación extiende el Container, tenemos acceso a todos los métodos de IoC directamente fuera de él.

Tras la instanciación del controlador, Laravel aprovecha la potencia de la reflexión de PHP para leer el tipo e inyectar la dependencia para usted. Está bien; Usted no tiene que escribir un solo enlace para permitir esto; ¡Es automatizado!


Redirecciones

Otra expectativa común de que te encontrarás escribiendo es aquella que asegura que el usuario sea redirigido a la ubicación correcta, tal vez al agregar una nueva entrada a la base de datos. ¿Cómo podemos lograr esto?

1
2
# app/tests/controllers/PostsControllerTest.php

3
4
public function testStore()
5
{
6
    $this->mock
7
         ->shouldReceive('create')
8
         ->once();
9
10
    $this->app->instance('Post', $this->mock);
11
12
    $this->call('POST', 'posts');
13
14
    $this->assertRedirectedToRoute('posts.index');
15
16
}

Suponiendo que estamos siguiendo un sabor restful, para agregar un nuevo post, hacemos POST a la colección, o posts (no confunda el método de solicitud POST con el nombre del recurso, que sólo tiene el mismo nombre).

1
2
$this->call('POST', 'posts');

Entonces, sólo necesitamos aprovechar otra de las aserciones de ayuda de Laravel, assertRedirectedToRoute.

Sugerencia: Cuando un recurso está registrado con Laravel (Route::resource()), el framework registrará automáticamente las rutas nombradas necesarias. Ejecute php artisan routes si alguna vez se olvida de cuáles son estos nombres.

Es posible que prefiera también asegurarse de que el $_POST superglobal se pasa al método create. A pesar de que no estamos enviando físicamente un formulario, todavía podemos permitir esto, a través del método Input::replace(), que nos permite "stub" esta array. Aquí está la prueba modificada, que utiliza el método with() de Mockery para verificar los argumentos pasados al método al que hace referencia shouldReceive.

1
2
# app/tests/controllers/PostsControllerTest.php

3
4
public function testStore()
5
{
6
    Input::replace($input = ['title' => 'My Title']);</p>
7
8
    $this->mock
9
         ->shouldReceive('create')
10
         ->once()
11
         ->with($input);
12
13
    $this->app->instance('Post', $this->mock);
14
15
    $this->call('POST', 'posts');
16
17
    $this->assertRedirectedToRoute('posts.index');
18
}

Rutas

Una cosa que no hemos considerado en esta prueba es la validación. Debe haber dos caminos separados a través del método store, dependiendo de si la validación pasa:

  1. Redirigir de nuevo al formulario "Crear publicación" y mostrar los errores de validación de formulario.
  2. Redirigir a la colección, o la ruta nombrada, posts.index.

Como una práctica recomendada, cada prueba debe representar un solo camino a través de su código.

Esta primera ruta será para validación fallida.

1
2
# app/tests/controllers/PostsControllerTest.php

3
4
public function testStoreFails()
5
{
6
    // Set stage for a failed validation

7
    Input::replace(['title' => '']);
8
9
    $this->app->instance('Post', $this->mock);
10
11
    $this->call('POST', 'posts');
12
13
    // Failed validation should reload the create form

14
    $this->assertRedirectedToRoute('posts.create');
15
16
    // The errors should be sent to the view

17
    $this->assertSessionHasErrors(['title']);
18
}

El fragmento de código anterior declara explícitamente qué errores deben existir. Alternativamente, puede omitir el argumento a assertSessionHasErrors, en cuyo caso se limitará a verificar que una bolsa de mensajes ha sido destellada (en la traducción, su redirección incluye withErrors($errors)).

Ahora, para la prueba que maneja la validación exitosa.

1
2
# app/tests/controllers/PostsControllerTest.php

3
4
public function testStoreSuccess()
5
{
6
    // Set stage for successful validation

7
    Input::replace(['title' => 'Foo Title']);</p>
8
9
    $this->mock
10
         ->shouldReceive('create')
11
         ->once();
12
13
    $this->app->instance('Post', $this->mock);
14
15
    $this->call('POST', 'posts');
16
17
    // Should redirect to collection, with a success flash message

18
    $this->assertRedirectedToRoute('posts.index', ['flash']);
19
}

El código de producción para estas dos pruebas podría ser:

1
2
# app/controllers/PostsController.php

3
4
public function store()
5
{
6
    $input = Input::all();
7
8
    // We'll run validation in the controller for convenience

9
    // You should export this to the model, or a service

10
    $v = Validator::make($input, ['title' => 'required']);
11
12
    if ($v->fails())
13
    {
14
        return Redirect::route('posts.create')
15
            ->withInput()
16
            ->withErrors($v->messages());
17
    }
18
19
    $this->post->create($input);
20
21
    return Redirect::route('posts.index')
22
        ->with('flash', 'Your post has been created!');
23
}

Observe cómo el Validator está anidado directamente en el controlador? En general, te recomiendo que abstraigas esto a un servicio. De esta forma, puede probar su validación de forma aislada de cualquier controlador o ruta. Sin embargo, dejemos las cosas como están por la simplicidad. Una cosa a tener en cuenta es que no estamos mock al Validator, aunque ciertamente podría hacerlo. Debido a que esta clase es una fachada, se puede intercambiar fácilmente con una versión burlada, a través del método shouldReceive de la Facade, sin que tengamos que preocuparnos por inyectar una instancia a través del constructor. ¡Victoria!

1
2
# app/controllers/PostsController.php

3
4
Validator::shouldReceive('make')
5
    ->once()
6
    ->andReturn(Mockery::mock(['fails' => 'true']));

De vez en cuando, usted encontrará que un método que necesita ser mock debe devolver un objeto, por sí mismo. Por suerte, con Mockery, esto es un muy facil: sólo necesitamos crear un simulacro anónimo, y pasar una array , que señala el nombre del método y el valor de respuesta, respectivamente. Como tal:

1
2
Mockery::mock(['fails' => 'true'])

Preparará un objeto, que contiene un método fails() que devuelve true.


Repositorios

Para permitir una flexibilidad óptima, en lugar de crear un enlace directo entre su controlador y un ORM, como Eloquent, es mejor codificar en una interfaz. La ventaja considerable de este enfoque es que, si tal vez necesitas intercambiar Eloquente por, digamos, Mongo o Redis, hacerlo literalmente requiere la modificación de una sola línea. Incluso mejor, el controlador no necesita ser tocado nunca.

Los repositorios representan la capa de acceso a datos de su aplicación.

¿Qué aspecto podría tener una interfaz para administrar la capa de base de datos de un Post? Esto debería hacerlo empezar.

1
2
<?php
3
4
# app/repositories/PostRepositoryInterface.php

5
6
interface PostRepositoryInterface {
7
8
    public function all();
9
10
    public function find($id);
11
12
    public function create($input);
13
14
}

Esto ciertamente puede ser ampliado, pero hemos añadido los métodos mínimos para la demostración: all, find y create. Observe que las interfaces del repositorio se almacenan en app/repositories. Debido a que esta carpeta no se carga automáticamente de forma predeterminada, necesitamos actualizar el archivo composer.json para que la aplicación le haga referencia.

1
2
// composer.json

3
4
"autoload": {
5
  "classmap": [
6
    // ....

7
    "app/repositories"
8
  ]
9
}

Cuando se añade una nueva clase a este directorio, no olvides al composer dump-autoload -o El indicador -o, (optimize) es opcional, pero siempre debe usarse, como una mejor práctica.

Si intenta inyectar esta interfaz en su controlador, Laravel fallara Adelante; Pruebalo y vea. Aquí está el PostController modificado, que se ha actualizado para inyectar una interfaz, en lugar del modelo Post Eloquent.

1
2
<?php
3
4
# app/controllers/PostsController.php

5
6
use Repositories\PostRepositoryInterface as Post;
7
8
class PostsController extends BaseController {
9
10
    protected $post;
11
12
    public function __construct(Post $post)
13
    {
14
        $this->post = $post;
15
    }
16
17
    public function index()
18
    {
19
        $posts = $this->post->all();
20
21
        return View::make('posts.index', ['posts' => $posts]);
22
    }
23
24
}

Si ejecuta el servidor y ve la salida, se encontrará con la temida (pero hermosa) página de error Whoops , declarando que "PostRepositoryInterface no es instanciable."

Not InstantiableNot InstantiableNot Instantiable

Si usted piensa en ello, por supuesto, ¡el marco esta fallando! Laravel es inteligente, pero no es un lector mental. Se necesita saber qué implementación de la interfaz debe utilizarse dentro del controlador.

Por ahora, vamos a agregar este enlace a app/routes.php. Posteriormente, en su lugar utilizaremos proveedores de servicios para almacenar este tipo de lógica.

1
2
# app/routes.php

3
4
App::bind(
5
    'Repositories\PostRepositoryInterface',
6
    'Repositories\EloquentPostRepository'
7
);

Verbalize esta llamada de función como, "Laravel, bebé, cuando necesitas una instancia de PostRepositoryInterface, quiero que uses EloquentPostRepository."

App/repositories/EloquentPostRepository simplemente será una envoltura alrededor de Eloquent que implementa PostRepositoryInterface. De esta manera, no estamos restringiendo la API (y cualquier otra implementación) a la interpretación de Eloquent; Podemos nombrar los métodos como quisiéramos.

1
2
<?php namespace Repositories;
3
4
# app/repositories/EloquentPostRepository.php

5
6
use Repositories\PostRepositoryInterface;
7
use Post;
8
9
class EloquentPostRepository implements PostRepositoryInterface {
10
11
  public function all()
12
  {
13
      return Post::all();
14
  }
15
16
  public function find($id)
17
  {
18
      return Post::find($id);
19
  }
20
21
  public function create($input)
22
  {
23
      return Post::create($input);
24
  }
25
26
}

Algunos podrían argumentar que el modelo Post debería ser inyectado en esta implementación con fines de testabilidad. Si usted está de acuerdo, simplemente inyectarlo a través del constructor, como habitualmente.

Eso es todo lo que debe tomar hacerlo! Actualiza el navegador y las cosas deberían volver a la normalidad. Sólo ahora su aplicación está mucho mejor estructurada y el controlador ya no está vinculado a Eloquent.

Imaginemos que, dentro de unos meses, su jefe le informará que necesita intercambiar Eloquent con Redis. Pues bien, ya que ha estructurado su aplicación de esta manera a prueba de futuro, sólo necesita crear la nueva app/repositories/RedisPostRepository:

1
2
<?php namespace Repositories;
3
4
# app/repositories/RedisPostRepository.php

5
6
use Repositories\PostRepositoryInterface;
7
8
class RedisPostRepository implements PostRepositoryInterface {
9
10
  public function all()
11
  {
12
      // return all with Redis

13
  }
14
15
  public function find($id)
16
  {
17
      // return find one with Redis

18
  }
19
20
  public function create($input)
21
  {
22
      // return create with Redis

23
  }
24
25
}

Y actualizar el enlace:

1
2
# app/routes.php

3
4
App::bind(
5
    'Repositories\PostRepositoryInterface',
6
    'Repositories\RedisPostRepository'
7
);

Al instante, ahora está aprovechando Redis en su controlador. ¿Noto cómo app/controllers/PostsController.php nunca se tocó? Esa es la belleza de la misma!


Estructura

Hasta ahora en esta lección, nuestra organización ha sido un poco carente. IoC enlaces en el archivo routes.php? Todos los repositorios agrupados en un directorio? Claro, eso puede funcionar al principio, pero, muy rápidamente, se hará evidente que esto no escala.

En la sección final de este artículo, PSR-er nuestro código, y aprovechar los proveedores de servicios para registrar cualquier consolidación aplicable.

PSR-0 define los requisitos obligatorios que deben cumplirse para la interoperabilidad del cargador automático.

Un cargador PSR-0 puede estar registrado con Composer, a través del objeto psr-0.

1
2
// composer.json

3
4
"autoload": {
5
    "psr-0": {
6
        "Way": "app/lib/"
7
    }
8
}

La sintaxis puede ser confusa al principio. Ciertamente fue para mí. Una manera fácil de descifrar "Way": "app/lib/" es pensar en ti mismo, "La carpeta base para el espacio de nombres Way se encuentra en app/lib." Por supuesto, reemplazar mi apellido con el nombre de su proyecto. La estructura de directorios para que coincida con esto sería:

  • app/
    • lib/
    • Way/

A continuación, en lugar de agrupar todos los repositorios en un directorio de repositories, un enfoque más elegante podría ser clasificarlos en varios directorios, de la siguiente manera:

  • app/
    • lib/
    • Way/
      • Storage/
      • Post/
        • PostRepositoryInterface.php
        • EloquentPostRepository.php

Es vital que nos adherimos a esta convención de nomenclatura y carpeta, si queremos que el autoloading funcione como se espera. Lo único que queda por hacer es actualizar los espacios de nombres para PostRepositoryInterface y EloquentPostRepository.

1
2
<?php namespace Way\Storage\Post;
3
4
# app/lib/Way/Storage/Post/PostRepositoryInterface.php

5
6
interface PostRepositoryInterface {
7
8
    public function all();
9
10
    public function find($id);
11
12
    public function create($input);
13
14
}

Y para la implementación:

1
2
<?php namespace Way\Storage\Post;
3
4
# app/lib/Way/Storage/Post/EloquentPostRepository.php

5
6
use Post;
7
8
class EloquentPostRepository implements PostRepositoryInterface {
9
10
    public function all()
11
    {
12
        return Post::all();
13
    }
14
15
    public function find($id)
16
    {
17
        return Post::find($id);
18
    }
19
20
    public function create($input)
21
    {
22
        return Post::create($input);
23
    }
24
25
}

Aquí vamos; Eso es mucho más limpio. Pero, ¿qué pasa con esas molestas fijaciones? El archivo de rutas puede ser un lugar conveniente para experimentar, pero tiene poco sentido almacenar allí permanentemente. En su lugar, utilizaremos los proveedores de servicios.

Los proveedores de servicios no son nada más que las clases de arranque que se pueden utilizar para hacer lo que quieras: registrar un enlace, conectar un evento, importar un archivo de rutas, etc.

El registro de un proveedor de servicios () será activado automáticamente por Laravel.

1
2
<?php namespace Way\Storage;
3
4
# app/lib/Way/Storage/StorageServiceProvider.php

5
6
use Illuminate\Support\ServiceProvider;
7
8
class StorageServiceProvider extends ServiceProvider {
9
10
    // Triggered automatically by Laravel

11
    public function register()
12
    {
13
        $this->app->bind(
14
            'Way\Storage\Post\PostRepositoryInterface',
15
            'Way\Storage\Post\EloquentPostRepository'
16
        );
17
    }
18
19
}

Para que este archivo sea conocido por Laravel, sólo es necesario incluirlo en app/config/app.php, dentro de la array de providers .

1
2
# app/config/app.php

3
4
'providers' => array(
5
    'Illuminate\Foundation\Providers\ArtisanServiceProvider',
6
    'Illuminate\Auth\AuthServiceProvider',
7
    // ...

8
    'Way\Storage\StorageServiceProvider'
9
)

Bueno; Ahora tenemos un archivo dedicado para registrar nuevos enlaces.

Actualización de las pruebas

Con nuestra nueva estructura en su lugar, en lugar de mock del modelo Eloquent, en sí, en lugar de eso podemos simular PostRepositoryInterface. He aquí un ejemplo de una prueba de este tipo:

1
2
# app/tests/controllers/PostsControllerTest.php

3
4
public function testIndex()
5
{
6
    $mock = Mockery::mock('Way\Storage\Post\PostRepositoryInterface');
7
    $mock->shouldReceive('all')->once();
8
9
    $this->app->instance('Way\Storage\Post\PostRepositoryInterface', $mock);
10
11
    $this->call('GET', 'posts');
12
13
    $this->assertViewHas('posts');
14
}

Sin embargo, podemos mejorar esto. Es lógico que cada método dentro de PostsControllerTest necesitará una versión mock del repositorio. Como tal, es mejor extraer parte de este trabajo de preparación en su propio método, así:

1
2
# app/tests/controllers/PostsControllerTest.php

3
4
public function setUp()
5
{
6
    parent::setUp();
7
8
    $this->mock('Way\Storage\Post\PostRepositoryInterface');
9
}
10
11
public function mock($class)
12
{
13
    $mock = Mockery::mock($class);
14
15
    $this->app->instance($class, $mock);
16
17
    return $mock;
18
}
19
20
public function testIndex()
21
{
22
    $this->mock->shouldReceive('all')->once();
23
24
    $this->call('GET', 'posts');
25
26
    $this->assertViewHas('posts');
27
}

No está mal, ¿hey?

Ahora, si quieres ser super-liviano, y estás dispuesto a agregar un toque de lógica de prueba a tu código de producción, ¡incluso podrías realizar tu mock dentro del modelo Eloquent! Esto permitiría:

1
2
Post::shouldReceive('all')->once();

Detrás de las escenas, esto mock PostRepositoryInterface, y actualizara la vinculación IoC. No se puede obtener mucho más legible que eso!

Permitir esta sintaxis sólo requiere actualizar el modelo Post o, mejor, un BaseModel que todos los modelos Eloquent se extienden. He aquí un ejemplo de lo anterior:

1
2
<?php
3
4
# app/models/Post.php

5
6
class Post extends Eloquent {
7
8
    public static function shouldReceive()
9
    {
10
        $class = get_called_class();
11
        $repo = "Way\\Storage\\{$class}\\{$class}RepositoryInterface";
12
        $mock = Mockery::mock($repo);
13
14
        App::instance($repo, $mock);
15
16
        return call_user_func_array([$mock, 'shouldReceive'], func_get_args());
17
    }
18
19
}

Si puede gestionar la batalla interna "Debería incrustar lógica de prueba en código de producción", verá que esto permite pruebas significativamente más legibles.

1
2
<?php
3
4
# app/tests/controllers/PostsControllerTest.php

5
6
class PostsControllerTest extends TestCase {
7
8
    public function tearDown()
9
    {
10
        Mockery::close();
11
    }
12
13
    public function testIndex()
14
    {
15
        Post::shouldReceive('all')->once();
16
17
        $this->call('GET', 'posts');
18
19
        $this->assertViewHas('posts');
20
    }
21
22
    public function testStoreFails()
23
    {
24
        Input::replace($input = ['title' => '']);
25
26
        $this->call('POST', 'posts');
27
28
        $this->assertRedirectedToRoute('posts.create');
29
        $this->assertSessionHasErrors();
30
    }
31
32
    public function testStoreSuccess()
33
    {
34
        Input::replace($input = ['title' => 'Foo Title']);
35
36
        Post::shouldReceive('create')->once();
37
38
        $this->call('POST', 'posts');
39
40
        $this->assertRedirectedToRoute('posts.index', ['flash']);
41
    }
42
43
}

Se siente bien, ¿no? Esperemos que este artículo no ha sido demasiado abrumador. La clave es aprender a organizar sus repositorios de tal manera que sean lo más fáciles de simular e inyectar en sus controladores. ¡Como resultado de ese esfuerzo, sus pruebas serán rápidas!

Este artículo es un extracto de mi próximo libro, Laravel Testing Decoded. ¡Manténgase atento para su lanzamiento en mayo de 2013!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.