2013-12-19 143 views
19

如何解決對可測試控制器的依賴性?具有相關性的可測試控制器

工作原理:一個URI被路由到一個控制器,一個控制器可能依賴於執行某個任務。

<?php 

require 'vendor/autoload.php'; 

/* 
* Registry 
* Singleton 
* Tight coupling 
* Testable? 
*/ 

$request = new Example\Http\Request(); 

Example\Dependency\Registry::getInstance()->set('request', $request); 

$controller = new Example\Controller\RegistryController(); 

$controller->indexAction(); 

/* 
* Service Locator 
* 
* Testable? Hard! 
* 
*/ 

$request = new Example\Http\Request(); 

$serviceLocator = new Example\Dependency\ServiceLocator(); 

$serviceLocator->set('request', $request); 

$controller = new Example\Controller\ServiceLocatorController($serviceLocator); 

$controller->indexAction(); 

/* 
* Poor Man 
* 
* Testable? Yes! 
* Pain in the ass to create with many dependencies, and how do we know specifically what dependencies a controller needs 
* during creation? 
* A solution is the Factory, but you would still need to manually add every dependencies a specific controller needs 
* etc. 
* 
*/ 

$request = new Example\Http\Request(); 

$controller = new Example\Controller\PoorManController($request); 

$controller->indexAction(); 

這是我的設計模式的例子解釋

註冊地:

  • 辛格爾頓
  • 緊耦合
  • 可測?沒有

服務定位器

  • 可測?硬/否(?)

窮人迪

  • 可測試
  • 很難與很多依賴

註冊表來維持

<?php 
namespace Example\Dependency; 

class Registry 
{ 
    protected $items; 

    public static function getInstance() 
    { 
     static $instance = null; 
     if (null === $instance) { 
      $instance = new static(); 
     } 

     return $instance; 
    } 

    public function set($name, $item) 
    { 
     $this->items[$name] = $item; 
    } 

    public function get($name) 
    { 
     return $this->items[$name]; 
    } 
} 

服務定位器

<?php 
namespace Example\Dependency; 

class ServiceLocator 
{ 
    protected $items; 

    public function set($name, $item) 
    { 
     $this->items[$name] = $item; 
    } 

    public function get($name) 
    { 
     return $this->items[$name]; 
    } 
} 

如何解決對可測試控制器的依賴關係?

+0

*「很難維護很多依賴項」* .. emm ..什麼依賴關係? –

+0

你的控制器返回什麼?你在測試什麼?以什麼方式? – mpm

回答

19

你在控制器中討論的依賴關係是什麼?

的主要解決辦法是:

  • 使用DI容器通過構造
  • 注入工廠服務的控制器中的特定服務直接傳遞

我要去嘗試分別詳細描述兩種方法。

注:所有的例子將留出互動與觀點,處理授權,處理服務工廠的依賴和其他細節工廠的


注射

The simplified引導階段,這有填充開始對控制器涉及的一部分,將看起來有點像這樣

$request = //... we do something to initialize and route this 
$resource = $request->getParameter('controller'); 
$command = $request->getMethod() . $request->getParameter('action'); 

$factory = new ServiceFactory; 
if (class_exists($resource)) { 
    $controller = new $resource($factory); 
    $controller->{$command}($request); 
} else { 
    // do something, because requesting non-existing thing 
} 

這種方法簡單地通過使在不同的用於延伸和/或替代模型層相關的代碼提供了一個明確的方式工廠作爲依賴。在控制器它會是這個樣子:

public function __construct($factory) 
{ 
    $this->serviceFactory = $factory; 
} 


public function postLogin($request) 
{ 
    $authentication = $this->serviceFactory->create('Authentication'); 
    $authentication->login(
     $request->getParameter('username'), 
     $request->getParameter('password') 
    ); 
} 

這意味着,以測試該控制器的方法,你就必須寫一個單元測試,嘲笑的$this->serviceFactory內容,創建實例和傳遞價值爲$request。該模擬將需要返回一個實例,它可以接受兩個參數。

注:給用戶的響應應該完全由視圖實例來處理,因爲創建響應是UI邏輯的一部分。請記住,HTTP位置標題爲也是的一種響應形式。

的單元測試用於這種控制器將如下所示:

public function test_if_Posting_of_Login_Works() 
{  
    // setting up mocks for the seam 

    $service = $this->getMock('Services\Authentication', ['login']); 
    $service->expects($this->once()) 
      ->method('login') 
      ->with($this->equalTo('foo'), 
        $this->equalTo('bar')); 

    $factory = $this->getMock('ServiceFactory', ['create']); 
    $factory->expects($this->once()) 
      ->method('create') 
      ->with($this->equalTo('Authentication')) 
      ->will($this->returnValue($service)); 

    $request = $this->getMock('Request', ['getParameter']); 
    $request->expects($this->exactly(2)) 
      ->method('getParameter') 
      ->will($this->onConsecutiveCalls('foo', 'bar')); 

    // test itself 

    $instance = new SomeController($factory); 
    $instance->postLogin($request); 

    // done 
} 

控制器應該是應用程序的最薄一部分。控制器的責任是:接受用戶輸入,並根據該輸入改變模型層的狀態(在極少數情況下 - 當前視圖)。而已。


使用DI容器

這另一種方法是..好..它基本上覆雜(減去在一個地方,添加更多的他人)的交易。它還繼承了一個真實 DI容器,而不是光榮的服務定位器,如Pimple

我的推薦:結帳Auryn

DI容器的作用是使用配置文件或反射,它確定要創建的實例的依賴關係。收集所說的依賴關係。並傳遞給實例的構造函數。

$request = //... we do something to initialize and route this 
$resource = $request->getParameter('controller'); 
$command = $request->getMethod() . $request->getParameter('action'); 

$container = new DIContainer; 
try { 
    $controller = $container->create($resource); 
    $controller->{$command}($request); 
} catch (FubarException $e) { 
    // do something, because requesting non-existing thing 
} 

因此,除了拋出異常的能力外,控制器的引導程序保持幾乎相同。

另外,在這一點上,您應該已經認識到,從一種方法切換到另一種方法大多需要完全重寫控制器(以及相關的單元測試)。

控制器在這種情況下,方法看起來是這樣的:

private $authenticationService; 

#IMPORTANT: if you are using reflection-based DI container, 
#then the type-hinting would be MANDATORY 
public function __construct(Service\Authentication $authenticationService) 
{ 
    $this->authenticationService = $authenticationService; 
} 

public function postLogin($request) 
{ 
    $this->authenticatioService->login(
      $request->getParameter('username'), 
      $request->getParameter('password') 
    ); 
} 

至於編寫一個測試,在這種情況下再次,所有你需要做的是對的隔離提供了一些嘲笑和簡單的驗證。但是,在這種情況下,單元測試更簡單

public function test_if_Posting_of_Login_Works() 
{  
    // setting up mocks for the seam 

    $service = $this->getMock('Services\Authentication', ['login']); 
    $service->expects($this->once()) 
      ->method('login') 
      ->with($this->equalTo('foo'), 
        $this->equalTo('bar')); 

    $request = $this->getMock('Request', ['getParameter']); 
    $request->expects($this->exactly(2)) 
      ->method('getParameter') 
      ->will($this->onConsecutiveCalls('foo', 'bar')); 

    // test itself 

    $instance = new SomeController($service); 
    $instance->postLogin($request); 

    // done 
} 

正如你所看到的,在這種情況下,你少了一個類來嘲笑。

雜記

  • 耦合至名(在例子 - 「認證」):

    正如你可能有通知,在這兩個例子您的代碼將被連接到名稱的服務,這是使用。即使您使用基於配置的DI容器(因爲它可能是in symfony),您仍將最終定義特定類的名稱。

  • DI容器不是魔法

    使用DI容器已經在過去幾年有所誇大。這不是一顆銀彈。我甚至會說:DI容器與SOLID不兼容。特別是因爲它們不適用於接口。您不能在代碼中真正使用多態行爲,這將通過DI容器進行初始化。

    然後存在基於配置的DI的問題。嗯..它很漂亮,而項目很小。但是隨着項目的增長,配置文件也會增長。您可以最終得到xml/yaml配置的光榮WALL,這隻能由項目中的一個人來理解。

    而第三個問題是複雜性。好的DI容器是而不是很容易製作。如果您使用第三方工具,則會引入更多風險。

  • 太多的依賴

    如果你的類有太多的依賴,那麼它是不是DI一個失敗的做法。相反,它是一個明確指示,你的班級做了太多事情。這違反了Single Responsibility Principle

  • 控制器實際上有(部分的)邏輯

    上面使用的例子是非常簡單的,並且其中通過單一服務與模型層進行交互。在現實世界中,您的控制器方法包含控制結構(循環,條件,東西)。

    最基本的使用案例是一個控制器,它將處理聯繫表單作爲「主題」下拉列表。大部分消息將被引導至與某些CRM進行通信的服務。但是,如果用戶選擇「報告錯誤」,則應該將消息傳遞給差異服務,該差異服務會自動在錯誤跟蹤器中創建故障單併發送一些通知。

  • 這是PHP單位

    使用PHPUnit框架編寫的的單元測試的例子。如果你正在使用一些其他的框架,或者手動編寫測試,你將不得不作出一些基本的改變

  • 你將有更多的測試

    的單元測試的例子是不是整組試驗你將擁有一個控制器的方法。特別是,當你有控制器是不平凡的。

其他材料

有一些.. EMM ...切主題。

準備迎接:無恥的自我推銷

  • dealing with access control in MVC-like architecture

    一些框架有推授權檢查的壞習慣(不以 「驗證」 混淆..不同的主題)在控制器中。除了完全愚蠢的事情之外,它還在控制器中引入了額外的依賴項(通常是全局範圍的)。

    存在使用類似的方法引入non-invasive logging

  • list of lectures

    它還挺針對誰想要了解MVC人另一篇文章,但材料實際上有在OOP和開發實踐普通教育。這個想法是,當你完成這個清單的時候,MVC和其他SoC的實現只會讓你去「」哦,這有一個名字?我認爲這只是常識。「

  • implementing model layer

    解釋了什麼是這些神奇的 「服務」 是在上面的描述。

+0

您正在批判Pimple,但DI容器在注入某個地方時都會進行服務位置(其好壞是另一個問題),DI容器與服務位置無關。 Pimple是一個使用服務位置來解析依賴關係圖的DI容器,但它仍然是一個DI容器。不管Pimple或DIC X如何解決依賴關係,它們都解決了依賴關係。 – mpm

+5

Pimple是一個服務定位器。**它不解決類的依賴關係**,因爲依賴關係是硬編碼的。請看[source](https://github.com/fabpot/Pimple/blob/master/lib/Pimple.php)。疙瘩是註冊表,其中一些包含的實體可以是匿名提供者而不是完整的對象,可以使用它自己。另外,DI容器不是什麼東西,你「注入」**。如果您注入DI容器,則它將成爲服務定位器。 –

+0

但我們同意某種方式,我們不同意服務位置和依賴注入的含義。您說Pimple是服務定位器。但我回答比任何DI容器可以做服務位置,因爲他們可以像任何其他對象注入。我不認爲Pimple的工作方式是相關的。是的,它使用服務位置,其中Auryn具有自動解決依賴關係的方法。但是這並不改變Auryn本身可以作爲依賴注入的事實,這就是問題(服務位置)。我知道疙瘩的來源,我將它移植到JS https://github.com/Mparaiso/Pimple.js – mpm

4

我從http://culttt.com/2013/07/15/how-to-structure-testable-controllers-in-laravel-4/

嘗試這樣做,你應該如何構造控制器,使其可測試?

測試您的控制器是建立一個堅實的Web應用程序的一個重要方面,但重要的是,你只測試您的應用程序中相應的位。

幸運的是,Laravel 4使您的控制器的關注非常容易。這使得測試你的控制器真的很簡單,只要你有正確的結構。

我應該在我的控制器中測試什麼?

之前,我到如何構建控制器可測性,首先它的重要,瞭解究竟是什麼,我們需要測試。

正如我在設置你的第一個Laravel 4控制器所提到的,控制器只應與模型和視圖之間移動數據有關。您無需驗證數據庫是否正在提取正確的數據,只需要Controller正在調用正確的方法即可。因此你的Controller測試不應該觸及數據庫。

這真的是我今天要展示你,因爲默認情況下它是很容易滑入控制器和模型耦合在一起。 不好的做法

的例子作爲說明什麼,我試圖避免的一種方式,這裏是一個控制器方法的一個例子:

public function index() 
{ 
    return User::all(); 
} 

這是一個不好的做法,因爲我們沒有辦法的嘲笑User::all();,所以相關的測試將被迫擊中數據庫。

依賴注入救援

爲了解決這個問題,我們要注入的依賴到控制器。依賴注入是將類傳遞給對象的實例,而不是讓該對象爲自己創建實例。

通過注入的依賴到溫控器,我們可以通過類模擬,而不是數據庫,而不是在我們的測試實際的數據庫對象本身。這意味着我們可以測試Controller的功能而無需觸摸數據庫。

作爲一般指引,任何地方,你看到正在創造另一個對象的實例類通常是一個跡象,表明這可能是處理與依賴注入更好。你永遠不希望你的對象緊密耦合,所以不要讓一個類實例化另一個類,以防止這種情況發生。

自動解析

Laravel 4具有處理注射扶養一個美麗的方式。這意味着您可以在許多場景下解決任何配置問題。

這意味着如果您通過構造函數傳遞一個類的另一個類的實例,Laravel會自動爲您注入該依賴項!

基本上,一切都將工作,沒有任何配置你的一部分。

注射數據庫到控制器

所以,現在你瞭解問題和解決方案的理論,我們現在可以修復控制器,它不連接到數據庫。

如果您還記得上週在Laravel Repositories上的帖子,您可能已經注意到我已經解決了這個問題。

所以不是這樣做的:

public function index() 
{ 
    return User::all(); 
} 

我所做的:

public function __construct(User $user) 
{ 
    $this->user = $user; 
} 

/** 
* Display a listing of the resource. 
* 
* @return Response 
*/ 
public function index() 
{ 
    return $this->user->all(); 
} 

在創建UserController類的__construct方法自動運行。 __construct方法注入一個User存儲庫的實例,然後在該類的$ this-> user屬性中設置該實例。

現在,只要您想在您的方法中使用數據庫,就可以使用$ this-> user實例。

懲戒在控制器的數據庫測試

當你來寫你的控制器測試的真正的奇蹟發生。既然您將數據庫實例傳遞給Controller,則可以模擬數據庫而不是實際觸及數據庫。這不僅可以提高性能,而且在測試之後你不會有任何測試數據。

我要做的第一件事是在測試目錄下創建一個名爲functional的新文件夾。我喜歡將Controller測試視爲功能測試,因爲我們正在測試傳入流量和渲染的視圖。

接下來我將創建一個名爲UserControllerTest.php文件,並寫入以下樣板代碼:

<?php 

class UserControllerTest extends TestCase { 

} 

如果你還記得回到我的崗位與嘲弄

嘲笑,什麼是測試驅動開發「,我談到了Mocks作爲依賴對象的替代品。

爲了在Cribbb中創建模擬測試,我將使用一個稱爲Mockery的神奇包。

Mockery允許您在項目中模擬對象,因此您不必使用真正的依賴關係。通過嘲笑一個對象,你可以告訴嘲笑你想調用哪種方法以及你想要返回什麼。

這使您能夠隔離您的依賴關係,因此您只需進行所需的控制器調用即可通過測試。例如,如果您想調用數據庫對象的all()方法,而不是實際觸及數據庫,則可以通過告訴Mockery要調用all()方法來嘲笑調用,並且它應該返回期望值。您不測試數據庫是否可以返回記錄,您只關心能否觸發該方法並處理返回值。

安裝Mockery 與所有優秀的PHP軟件包一樣,Mockery可以通過Composer安裝。

要通過作曲家安裝嘲弄,下面一行添加到您的composer.json文件:

"require-dev": { 
    "mockery/mockery": "dev-master" 
} 

接下來,安裝包:

composer install --dev 

設置嘲弄

現在設置Mockery,我們必須在測試文件中創建幾個設置方法:

public function setUp() 
{ 
    parent::setUp(); 

    $this->mock = $this->mock('Cribbb\Storage\User\UserRepository'); 
} 

public function mock($class) 
{ 
    $mock = Mockery::mock($class); 

    $this->app->instance($class, $mock); 

    return $mock; 
} 

setUp()方法在任何測試之前運行。在這裏,我們抓取UserRepository的副本並創建一個新的模擬。

mock()方法,$this->app->instance告訴Laravel的IoC容器的$mock實例綁定到UserRepository類。這意味着只要Laravel想要使用這個類,它就會使用模擬。 編寫第一個控制器檢測

接下來,你可以寫你的第一個控制器測試:

public function testIndex() 
{ 
    $this->mock->shouldReceive('all')->once(); 

    $this->call('GET', 'user'); 

    $this->assertResponseOk(); 
} 

在這個測試中,我問了模擬一次在UserRepository調用all()方法。然後我使用GET請求調用頁面,然後聲明響應正常。

結論

測試控制器不應該是困難或複雜,因爲它是做出來是。只要您隔離依賴關係並僅測試正確的位,測試控制器應該非常簡單。

這可以幫助你。