2014-02-13 66 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