具有依赖项的可测试控制器

用户名

如何解决对可测试控制器的依赖关系?

工作原理:URI被路由到Controller,Controller可能具有依赖关系来执行特定任务。

<?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];
    }
} 

如何解决对可测试控制器的依赖关系?

毛圈

您在控制器中谈论的依赖关系是什么?

到主要解决方案是:

  • 通过构造函数在控制器中注入服务工厂
  • 使用DI容器直接传递特定服务

我将尝试分别详细描述这两种方法。

注意:所有示例都将省略与视图的交互,授权的处理,处理服务工厂的依赖关系以及其他细节


注塑厂

引导阶段简化部分,涉及启动控制器的工作,看起来像这样

$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 Location标头也是一种响应形式。

此类控制器的单元测试如下所示:

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容器(如symfony中可能的那样),您仍然最终将定义特定类的名称。

  • DI容器不是魔术

    在过去的几年中,DI容器的使用已经大肆宣传。这不是灵丹妙药。我什至可以说:DI容器与SOLID不兼容特别是因为它们不适用于接口。您不能在代码中真正使用多态行为,该行为将由DI容器初始化。

    然后是基于配置的DI的问题。好吧..它只是项目很小的时候的美丽。但是随着项目的增长,配置文件也会增长。您可以得到光荣的xml / yaml配置WALL,项目中只有一个人可以理解。

    第三个问题是复杂性。好的DI容器并不容易制造。而且,如果您使用第三方工具,则会带来其他风险。

  • 依赖性过多

    如果您的类具有过多的依赖关系,那么按照惯例,这并不是DI的失败。相反,这是一个明确的指示,表明您的课程在做太多事情。这违反了单一责任原则

  • 控制器实际上具有(某些)逻辑

    上面使用的示例非常简单,并且可以通过单个服务与模型层进行交互。在现实世界中,您的控制器方法包含控制结构(循环,条件,填充)。

    最基本的用例是使用“主题”下拉列表处理联系表单的控制器。大多数消息将被定向到与某些CRM进行通信的服务。但是,如果用户选择“报告错误”,则该消息应传递到差异服务,该服务将在错误跟踪器中自动创建票证并发送一些通知。

  • 这是PHP单元

    单元测试的示例是使用PHPUnit框架编写的如果您使用其他框架或手动编写测试,则必须进行一些基本更改

  • 您将进行更多测试

    单元测试示例不是控制器方法要进行的全部测试。特别是当您拥有不平凡的控制器时。

其他材质

有一些.. emm ...切线主题。

Brace for: shameless self-promotion

  • 处理类似于MVC的体系结构中的访问控制

    一些框架具有在控制器中推送授权检查的讨厌习惯(不要与“ authentication” ..不同的主题混淆)。除了要做完全愚蠢的事情之外,它还引入了控制器中的其他依赖项(通常-全局范围)。

    还有另一篇文章使用类似的方法介绍了非侵入式测井

  • 讲座清单

    它是针对想要学习MVC的人的,但是实际上那里有面向OOP和开发实践的通识教育材料。这个想法是,当您完成该列表时,MVC和其他SoC实现将只会使您走到“哦,这有个名字吗?我认为这只是常识。”

  • 实现模型层

    解释上面描述中的那些神奇的“服务”。

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

具有依赖项的可测试控制器

来自分类Dev

如何使用 Moq 对具有三个依赖项的 MVC 控制器进行单元测试

来自分类Dev

如何在控制器测试中注入依赖项?

来自分类Dev

依赖项失败的 Spring 控制器测试

来自分类Dev

如何使用Simple Injector解析具有值类型依赖项的MVC控制器?

来自分类Dev

具有Jasmine中的服务依赖关系的AngularJS单元测试控制器

来自分类Dev

AngularJS中的单元测试错误-具有很多依赖性的控制器

来自分类Dev

如何在具有AutoFac的Umbraco 7控制器中通过依赖项注入使用IMediator(MediatR)?

来自分类Dev

具有测试依赖项的Pub变压器

来自分类Dev

具有会话属性的MVC控制器测试

来自分类Dev

具有会话属性的MVC控制器测试

来自分类Dev

单元测试控制器,在控制器中具有$ state.go方法

来自分类Dev

实施Spring控制器的单元测试时缺少hasSize()和hasProperty()依赖项

来自分类Dev

当我向控制器添加新的依赖项时,单元测试中断

来自分类Dev

在不模拟依赖项的情况下测试角度控制器

来自分类Dev

单元测试注入依赖项,例如用于angularJS中服务的控制器

来自分类Dev

没有控制器的依赖注入

来自分类Dev

我如何知道可以注入控制器的依赖项?

来自分类Dev

在哪里加载控制器依赖项类?

来自分类Dev

如何在指令控制器中注入依赖项

来自分类Dev

在哪里加载控制器依赖项类?

来自分类Dev

控制器中的角度添加依赖项

来自分类Dev

如何从依赖项中使用控制器服务 api

来自分类Dev

如何正确配置模拟依赖项以使用 Autofac 测试 Web Api (ASP.NET Core) 控制器

来自分类Dev

具有依赖项的Symfony验证器

来自分类Dev

通过服务依赖注入进行控制器测试

来自分类Dev

通过服务依赖注入以进行控制器测试

来自分类Dev

具有多个线程和不同视图控制器的一项任务

来自分类Dev

具有多个控制器的MVC

Related 相关文章

  1. 1

    具有依赖项的可测试控制器

  2. 2

    如何使用 Moq 对具有三个依赖项的 MVC 控制器进行单元测试

  3. 3

    如何在控制器测试中注入依赖项?

  4. 4

    依赖项失败的 Spring 控制器测试

  5. 5

    如何使用Simple Injector解析具有值类型依赖项的MVC控制器?

  6. 6

    具有Jasmine中的服务依赖关系的AngularJS单元测试控制器

  7. 7

    AngularJS中的单元测试错误-具有很多依赖性的控制器

  8. 8

    如何在具有AutoFac的Umbraco 7控制器中通过依赖项注入使用IMediator(MediatR)?

  9. 9

    具有测试依赖项的Pub变压器

  10. 10

    具有会话属性的MVC控制器测试

  11. 11

    具有会话属性的MVC控制器测试

  12. 12

    单元测试控制器,在控制器中具有$ state.go方法

  13. 13

    实施Spring控制器的单元测试时缺少hasSize()和hasProperty()依赖项

  14. 14

    当我向控制器添加新的依赖项时,单元测试中断

  15. 15

    在不模拟依赖项的情况下测试角度控制器

  16. 16

    单元测试注入依赖项,例如用于angularJS中服务的控制器

  17. 17

    没有控制器的依赖注入

  18. 18

    我如何知道可以注入控制器的依赖项?

  19. 19

    在哪里加载控制器依赖项类?

  20. 20

    如何在指令控制器中注入依赖项

  21. 21

    在哪里加载控制器依赖项类?

  22. 22

    控制器中的角度添加依赖项

  23. 23

    如何从依赖项中使用控制器服务 api

  24. 24

    如何正确配置模拟依赖项以使用 Autofac 测试 Web Api (ASP.NET Core) 控制器

  25. 25

    具有依赖项的Symfony验证器

  26. 26

    通过服务依赖注入进行控制器测试

  27. 27

    通过服务依赖注入以进行控制器测试

  28. 28

    具有多个线程和不同视图控制器的一项任务

  29. 29

    具有多个控制器的MVC

热门标签

归档