2014-02-13 34 views
6

我试图在控制器中正确地模拟对Eloquent模型的链接调用。在我的控制器中,我使用依赖注入来访问模型,所以它应该很容易模拟,但是我不确定如何测试链式调用并使其正确工作。这一切都在Laravel 4.1中使用PHPUnit和Mockery。在Mockery中测试链式方法调用

控制器:

<?php 

class TextbooksController extends BaseController 
{ 
    protected $textbook; 

    public function __construct(Textbook $textbook) 
    { 
     $this->textbook = $textbook; 
    } 

    public function index() 
    { 
     $textbooks = $this->textbook->remember(5) 
      ->with('user') 
      ->notSold() 
      ->take(25) 
      ->orderBy('created_at', 'desc') 
      ->get(); 

     return View::make('textbooks.index', compact('textbooks')); 
    } 
} 

控制器测试:

<?php 

class TextbooksControllerText extends TestCase 
{ 
    public function __construct() 
    { 
     $this->mock = Mockery::mock('Eloquent', 'Textbook'); 
    } 

    public function tearDown() 
    { 
     Mockery::close(); 
    } 

    public function testIndex() 
    { 
     // Here I want properly mock my chained call to the Textbook 
     // model. 

     $this->action('GET', '[email protected]'); 

     $this->assertResponseOk(); 
     $this->assertViewHas('textbooks'); 
    } 
} 

我一直试图通过在测试中$this->action()调用之前把这个代码来实现这一点。

$this->mock->shouldReceive('remember')->with(5)->once(); 
$this->mock->shouldReceive('with')->with('user')->once(); 
$this->mock->shouldReceive('notSold')->once(); 
$this->app->instance('Textbook', $this->mock); 

但是,这会导致错误Fatal error: Call to a member function with() on a non-object in /app/controllers/TextbooksController.php on line 28

我也尝试了一个链式替代希望它会做的伎俩。

$this->mock->shouldReceive('remember')->with(5)->once() 
    ->shouldReceive('with')->with('user')->once() 
    ->shouldReceive('notSold')->once(); 
$this->app->instance('Textbook', $this->mock); 

什么是我应该采取的最好的方法来测试与Mockery这种链式方法调用。

+0

请阅读文档 https://github.com/padraic/mockery#mocking-demeter-chains-and-fluent - 接口 – Shakil

回答

3

我是很新的测试自己,这整个的答案可能是错在大多数人的眼里,但我看到人们检测错误的东西的流行。如果你测试一个方法所做的一切,那么你就不是测试,而只是写一个方法两次。

你应该把你的代码看作是黑盒子的东西 - 当你编写测试时,不要想知道里面发生了什么。调用给定输入的方法,期望输出。有时你需要确保发生了某些其他的效果,这就是shouldReceive的东西了。但是它又比这个集合链测试更高级 - 你应该测试代码去做这个代码做的事情,但是确切地说,代码本身发生。因此,收集链应该以某种方式提取到其他方法,并且应该简单地测试该方法是否被调用。

越是测试实际的书面代码(而不是代码的目的),你会遇到的问题越多。例如,如果您需要更新代码以不同的方式执行相同的操作(可能是remember(6)而不是remember(5)作为该链的一部分或某些内容),则还必须更新测试以确保现在调用remember(6),而当您不应该我们不会去测试它。

这个建议并不仅仅适用于链式方法,它在任何时候都可以确保各种对象在测试给定方法时调用了各种方法。

虽然我不喜欢术语“红,绿,重构”你应该在这里把它看成有两点在您的测试方法失败:

  • 红/绿:当你第一次写测试失败,你的代码不应该包含所有这些shouldReceive(如果有意义的话,可能是一两个),如果是这样的话,那么你不是在编写测​​试,而是在编写代码。实际上,这表示您先编写代码然后再进行测试以适应代码,这是针对测试优先的TDD的。
  • 重构:假设你已经编写了代码,然后测试,以适应代码(或嘿莫名其妙地设法猜测什么应该接受写在你的测试,代码只是神奇的成果)。这很糟糕,但让我们说你做到了,因为它不是世界末日。您现在需要重构,但如果不更改测试,则无法进行重构。你的测试与代码紧密结合,任何重构都会破坏测试。这也是对TDD的想法。

即使您不遵循测试优先的TDD,您至少应该意识到重构步骤应该可行而不会中断测试。

无论如何,那只是我的老婆。

+0

另外我知道这并不直接回答问题,但我认为这是一个很好的答案,因为它回答了代码的更广泛的一面,对整个社区都有帮助。 – alexrussell

+1

是的,这是一个很好的答案。其实通过阅读它让我觉得有点愚蠢,因为现在看起来更加明显;测试最终结果是否符合预期,并且只在测试代码的较高级别(如果它们对该进程至关重要)时才进行测试。 我会在下面留下我的答案,因为它在技术上实现了我最初寻找的结果,但这是错误的方法。 – Dwight

+0

我们一起回答问题的两个方面:) – alexrussell

1

我发现了这种技术,但我不喜欢它。这非常详细。我认为必须有一个更简单/更简单的方法来实现这一点。

在构造函数中:

$this->collection = Mockery::mock('Illuminate\Database\Eloquent\Collection')->shouldDeferMissing(); 

在测试:

$this->mock->shouldReceive('remember')->with(5)->andReturn($this->mock); 
$this->mock->shouldReceive('with')->with('user')->andReturn($this->mock); 
$this->mock->shouldReceive('notSold')->andReturn($this->mock); 
$this->mock->shouldReceive('take')->with(25)->andReturn($this->mock); 
$this->mock->shouldReceive('orderBy')->with('created_at', 'DESC')->andReturn($this->mock); 
$this->mock->shouldReceive('get')->andReturn($this->collection); 
15

最初是一个评论,但移动回答,使代码易读!

我朝@alexrussell's answer瘦过,虽然中间立场是:

$this->mock->shouldReceive('remember->with->notSold->take->orderBy->get') 
    ->andRe‌​turn($this->collection); 
+1

这个工作,但我注意到它导致代码覆盖失败(如果你使用的话) – dwenaus

+0

我不知道,所以谢谢指出。另一个不被吸入被测单位内脏的原因:) – petercoles

+0

@petercoles 3年后,但$ this-> mock不可用。如何实例化? – Mehrdad