2013-12-19 144 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请求调用页面,然后声明响应正常。

结论

测试控制器不应该是困难或复杂,因为它是做出来是。只要您隔离依赖关系并仅测试正确的位,测试控制器应该非常简单。

这可以帮助你。