php unit-testing dependency-injection laravel-4 mockery

php - Inyección de dependencia de Laravel: ¿Cuándo tienes que hacerlo? ¿Cuándo puedes burlarte de las fachadas? ¿Ventajas de cualquiera de los métodos?



unit-testing dependency-injection (1)

He estado usando Laravel por un tiempo y he estado leyendo mucho acerca de Inyección de dependencia y un código comprobable. He llegado a un punto de confusión cuando hablo de fachadas y objetos burlados. Veo dos patrones:

class Post extends Eloquent { protected $guarded = array(); public static $rules = array(); }

Este es mi modelo posterior. Podría ejecutar Post::all(); Para obtener todas las publicaciones de mi blog. Ahora quiero incorporarlo en mi controlador.

Opción # 1: inyección de dependencia

Mi primer instinto sería inyectar el modelo Post como una dependencia:

class HomeController extends BaseController { public function __construct(Post $post) { $this->post = $post; } public function index() { $posts = $this->posts->all(); return View::make( ''posts'' , compact( $posts ); } }

Mi prueba de unidad se vería así:

<?php use /Mockery; class HomeControllerTest extends TestCase { public function tearDown() { Mockery::close(); parent::tearDown(); } public function testIndex() { $post_collection = new StdClass(); $post = Mockery::mock(''Eloquent'', ''Post'') ->shouldRecieve(''all'') ->once() ->andReturn($post_collection); $this->app->instance(''Post'',$post); $this->client->request(''GET'', ''posts''); $this->assertViewHas(''posts''); } }

Opción # 2: Falsas de fachada

class HomeController extends BaseController { public function index() { $posts = Post::all(); return View::make( ''posts'' , compact( $posts ); } }

Mi prueba de unidad se vería así:

<?php use /Mockery; class HomeControllerTest extends TestCase { public function testIndex() { $post_collection = new StdClass(); Post::shouldRecieve(''all'') ->once() ->andReturn($post_collection); $this->client->request(''GET'', ''posts''); $this->assertViewHas(''posts''); } }

Entiendo ambos métodos pero no entiendo por qué debería o cuándo debo usar un método sobre el otro. Por ejemplo, he intentado usar la ruta DI con la clase Auth , pero no funciona, así que tengo que usar las Falsas de fachada. Cualquier calcificación sobre este tema sería muy apreciada.


Aunque usa la inyección de dependencia en la Opción # 1, su controlador aún está acoplado con el ORM Eloquent. (Tenga en cuenta que evito usar el término Modelo aquí porque en MVC el Modelo no es solo una clase o un objeto, sino una capa. Es su lógica empresarial).

La inyección de dependencia permite la inversión de dependencia, pero no son lo mismo. De acuerdo con el principio de inversión de dependencia, tanto el código de nivel alto como el bajo deben depender de las abstracciones. En su caso, el código de alto nivel es su controlador y el código de bajo nivel es el ORM Eloquent que obtiene datos de MySQL, pero como puede ver, ninguno de ellos depende de las abstracciones.

Como consecuencia, no puede cambiar su capa de acceso a datos sin afectar su controlador. ¿Cómo cambiaría, por ejemplo, de MySQL a MongoDB o al sistema de archivos? Para hacer esto tienes que usar repositorios (o como quieras llamarlo).

Así que cree una interfaz de repositorios que todas sus implementaciones concretas de repositorios (MySQL, MongoDB, Sistema de archivos, etc.) deberían implementar.

interface PostRepositoriesInterface { public function getAll(); }

y luego cree su implementación concreta, por ejemplo, para MySQL

class DbPostRepository implements PostRepositoriesInterface { public function getAll() { return Post::all()->toArray(); /* Why toArray()? This is the L (Liskov Substitution) in SOLID. Any implementation of an abstraction (interface) should be substitutable in any place that the abstraction is accepted. But if you just return Post:all() how would you handle the situation where another concrete implementation would return another data type? Probably you would use an if statement in the controller to determine the data type but that''s far from ideal. In PHP you cannot force the return data type so this is something that you have to keep in mind.*/ } }

Ahora su controlador debe tipear la interfaz y no la implementación concreta. De esto se trata el "Código en una interfaz y no en la implementación". Esta es la inversión de dependencia.

class HomeController extends BaseController { public function __construct(PostRepositoriesInterface $repo) { $this->repo= $repo; } public function index() { $posts = $this->repo->getAll(); return View::make( ''posts'' , compact( $posts ) ); } }

De esta manera, su controlador se desacopla de su capa de datos. Está abierto por extensión pero cerrado por modificación. Puede cambiar a MongoDB o al sistema de archivos creando una nueva implementación concreta de PostRepositoriesInterface (por ejemplo, MongoPostRepository) y cambiar solo el enlace desde (Tenga en cuenta que no uso ningún espacio de nombres aquí):

App:bind(''PostRepositoriesInterface'',''DbPostRepository'');

a

App:bind(''PostRepositoriesInterface'',''MongoPostRepository'');

En una situación ideal, su controlador debe contener solo aplicaciones y no lógica empresarial. Si alguna vez desea llamar a un controlador desde otro controlador, es una señal de que ha hecho algo mal. En este caso sus controladores contienen demasiada lógica.

Esto también facilita las pruebas. Ahora puedes probar tu controlador sin llegar a la base de datos. Tenga en cuenta que una prueba del controlador solo debe probar si el controlador funciona correctamente, lo que significa que el controlador llama al método correcto, obtiene los resultados y lo pasa a la vista. En este punto no está probando la validez de los resultados. Esto no es responsabilidad del controlador.

public function testIndexActionBindsPostsFromRepository() { $repository = Mockery::mock(''PostRepositoriesInterface''); $repository->shouldReceive(''all'')->once()->andReturn(array(''foo'')); App::instance(''PostRepositoriesInterface'', $repository); $response = $this->action(''GET'', ''HomeController@index''); $this->assertResponseOk(); $this->assertViewHas(''posts'', array(''foo'')); }

EDITAR

Si eliges ir con la opción # 1, puedes probarla así

class HomeControllerTest extends TestCase { public function __construct() { $this->mock = Mockery::mock(''Eloquent'', ''Post''); } public function tearDown() { Mockery::close(); } public function testIndex() { $this->mock ->shouldReceive(''all'') ->once() ->andReturn(''foo''); $this->app->instance(''Post'', $this->mock); $this->call(''GET'', ''posts''); $this->assertViewHas(''posts''); } }