Laravel依赖注入:你什么时候需要?你什么时候可以模仿外墙?两种方法的优点?

ang*_*inb 16 php unit-testing dependency-injection mockery laravel-4

我已经使用Laravel一段时间了,我已经阅读了很多关于依赖注入的可测试代码.在谈到Facades和Mocked Objects时,我已经陷入了困惑.我看到两种模式:

class Post extends Eloquent {

    protected $guarded = array();

    public static $rules = array();

}
Run Code Online (Sandbox Code Playgroud)

这是我的帖子模型.我可以跑来Post::all();从我的博客上获取所有帖子.现在我想将它合并到我的控制器中.


选项#1:依赖注入

我的第一直觉是将Post模型作为依赖注入:

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 );
    }

}
Run Code Online (Sandbox Code Playgroud)

我的单元测试看起来像这样:

<?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');
    }
}
Run Code Online (Sandbox Code Playgroud)

选项#2:门面模拟

class HomeController extends BaseController {


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

}
Run Code Online (Sandbox Code Playgroud)

我的单元测试看起来像这样:

<?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');
    }
}
Run Code Online (Sandbox Code Playgroud)

我理解这两种方法,但我不明白为什么我应该或何时应该使用一种方法而不是另一种方法.例如,我试图在Auth课程中使用DI路线,但它不起作用所以我必须使用Facade Mocks.任何关于这个问题的钙化都将非常感激.

tli*_*kos 35

虽然您在选项#1上使用依赖注入,但您的控制器仍然与Eloquent ORM耦合.(请注意,我在这里避免使用术语Model,因为在MVC中,Model不仅仅是一个类或一个对象而是一个层.这是您的业务逻辑.).

依赖注入允许依赖性反转,但它们不是同一个东西.根据依赖性倒置原则,高级和低级代码都应该依赖于抽象.在您的情况下,高级代码是您的控制器,低级代码是从MySQL获取数据的Eloquent ORM,但正如您所看到的,它们都不依赖于抽象.

因此,您无法在不影响控制器的情况下更改数据访问层.您将如何从MySQL更改为MongoDB或文件系统?要做到这一点,你必须使用存储库(或任何你想要调用它).

因此,创建一个存储库接口,所有具体的存储库实现(MySQL,MongoDB,文件系统等)都应该实现.

interface PostRepositoriesInterface {

    public function getAll();
}
Run Code Online (Sandbox Code Playgroud)

然后创建您的具体实现,例如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.*/
    }
}
Run Code Online (Sandbox Code Playgroud)

现在您的控制器必须键入提示界面而不是具体实现.这就是"接口上的代码而不是实现"的全部内容.这是依赖倒置.

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 ) );
    }

}
Run Code Online (Sandbox Code Playgroud)

这样,您的控制器就会与数据层分离.它是开放的扩展,但关闭修改.您可以通过创建一个新的具体实施PostRepositoriesInterface(如MongoPostRepository)的切换到MongoDB的或文件系统,只改变从绑定(请注意,我不使用这里的任何命名空间):

App:bind('PostRepositoriesInterface','DbPostRepository');
Run Code Online (Sandbox Code Playgroud)

App:bind('PostRepositoriesInterface','MongoPostRepository');
Run Code Online (Sandbox Code Playgroud)

在理想情况下,您的控制器应该只包含应用程序而不包含业务逻辑.如果你发现自己想要从另一个控制器调用一个控制器,那就表明你做错了什么.在这种情况下,您的控制器包含太多逻辑.

这也使测试更容易.现在,您可以在不实际访问数据库的情况下测试控制器.请注意,控制器测试必须仅在控制器正常运行时进行测试,这意味着控制器调用正确的方法,获取结果并将其传递给视图.此时您没有测试结果的有效性.这不是控制人的责任.

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')); 
}
Run Code Online (Sandbox Code Playgroud)

编辑

如果您选择使用选项#1,您可以像这样测试它

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');
  }

}
Run Code Online (Sandbox Code Playgroud)