16

私はしばらくLaravelを使用しており、テスト可能なコードである依存性注入について多くのことを読んでいます。Facades と Mocked Objects について話すとき、私は混乱するところまで来ました。2 つのパターンが表示されます。

class Post extends Eloquent {

    protected $guarded = array();

    public static $rules = array();

}

これが私の投稿モデルです。実行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 );
    }

}

私の単体テストは次のようになります。

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

オプション #2: ファサード モック

class HomeController extends BaseController {


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

}

私の単体テストは次のようになります。

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

私は両方の方法を理解していますが、なぜ、またはいつ一方の方法を他方よりも優先して使用する必要があるのか​​ を理解していません。たとえば、Authクラスで DI ルートを使用しようとしましたが、機能しないため、Facade モックを使用する必要があります。この問題に関する石灰化は大歓迎です。

4

1 に答える 1

36

オプション #1 で依存性注入を使用していますが、コントローラーは依然として Eloquent ORM と結合されています。(MVC ではモデルは単なるクラスやオブジェクトではなくレイヤーであるため、ここではモデルという用語を使用しないことに注意してください。これはビジネス ロジックです。)

依存性注入は依存性反転を可能にしますが、それらは同じものではありません。依存性逆転の原則によれば、高レベルと低レベルの両方のコードが抽象化に依存する必要があります。あなたの場合、高レベルのコードはコントローラーであり、低レベルのコードは MySQL からデータをフェッチする Eloquent ORM ですが、ご覧のとおり、抽象化に依存するものはありません。

結果として、コントローラーに影響を与えずにデータ アクセス レイヤーを変更することはできません。たとえば、MySQL から MongoDB またはファイル システムに変更するにはどうすればよいでしょうか? これを行うには、リポジトリ (または任意の名前) を使用する必要があります。

したがって、すべての具体的なリポジトリ実装 (MySQL、MongoDB、ファイル システムなど) が実装する必要があるリポジトリ インターフェイスを作成します。

interface PostRepositoriesInterface {

    public function getAll();
}

次に、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.*/
    }
}

ここで、コントローラーは具体的な実装ではなく、インターフェイスのヒントを入力する必要があります。これが「実装ではなくインターフェース上のコード」のすべてです。これが依存関係の逆転です。

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

}

このようにして、コントローラーはデータ層から分離されます。拡張用に開いていますが、変更用に閉じています。PostRepositoriesInterface の新しい具体的な実装 (MongoPostRepository など) を作成することで、MongoDB またはファイル システムに切り替えることができます (ここでは名前空間を使用しないことに注意してください)。

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

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

理想的な状況では、コントローラーにはアプリケーションのみが含まれ、ビジネス ロジックは含まれないようにする必要があります。別のコントローラーからコントローラーを呼び出したいと思ったことがある場合は、何か間違ったことをしている兆候です。この場合、コントローラーに含まれるロジックが多すぎます。

これにより、テストも容易になります。これで、実際にデータベースにアクセスせずにコントローラーをテストできるようになりました。コントローラーのテストは、コントローラーが適切に機能する場合にのみテストする必要があることに注意してください。つまり、コントローラーが正しいメソッドを呼び出し、結果を取得してビューに渡す必要があります。この時点では、結果の有効性をテストしていません。これは管理者の責任ではありません。

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

編集

オプション#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');
  }

}
于 2014-02-26T07:43:25.610 に答える