具有依赖性的可测试控制器 [英] Testable Controllers with dependencies

查看:262
本文介绍了具有依赖性的可测试控制器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



工作原理:URI路由到控制器,控制器可能具有执行某项任务的依赖关系。

 <?php 

require'vendor / autoload.php;

/ *
*注册表
* Singleton
*紧耦合
*可测试?
* /

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

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

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

$ controller-> indexAction();

/ *
*服务定位器
*
*可测试?硬!
*
* /

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

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

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

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

$ controller-> indexAction();

/ *
*可怜的人
*
*可测试?是!
*在屁股中痛苦地创建许多依赖关系,我们如何在创建过程中明确指出控制器需要
*的依赖关系?
*解决方案是工厂,但您仍然需要手动添加特定控制器需要
*等的每个依赖项。
*
* /

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

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

$ controller-> indexAction();

这是我对设计模式示例的解释



注册表:




  • Singleton

  • 紧耦合

  • 可测试?否



服务定位器




  • 可测试?硬/否(?)



可怜的人Di




  • 可测试

  • 很难维持许多依赖关系



注册表

 <?php 
命名空间Example\Dependency;

class注册表
{
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 
命名空间Example\Dependency;

class ServiceLocator
{
protected $ items;

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

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

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

解决方案

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

主要解决方案是:




  • 在控制器中注入服务工厂,构造函数

  • 使用DI容器直接传递特定服务



我要去尝试详细描述这两种方法。


注意:所有示例将不在互动查看,处理授权,处理服务工厂的依赖和其他细节



<

注入工厂



引导阶段的简化部分, o控制器,看起来有点像这样

  $ request = // ...我们做一些初始化和路由这个
$ resource = $ request-> getParameter('controller');
$ command = $ request-> getMethod()。 $请求 - >的getParameter(动作);

$ factory = new ServiceFactory;
if(class_exists($ resource)){
$ controller = new $ resource($ factory);
$ controller-> {$ command}($ request);
} else {
//做某事,因为请求不存在的东西
}

这种方法提供了一种通过简单地传递不同工厂作为依赖关系来扩展和/或替代模型层相关代码的清晰方式。在控制器中,它看起来像这样:

  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()
{
//设置对于接缝

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

$ factory = $ this-> getMock('ServiceFactory',['create']);
$ factory-> expects($ this-> once())
- > method('create')
- > with($ this->认证'))
- >将($ this-> returnValue($ service));

$ request = $ this-> getMock('Request',['getParameter']);
$ request-> expects($ this-> exact(2))
- >方法('getParameter')
- >将($ this-> onConsecutiveCalls 'foo','bar'));

//测试本身

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

//完成
}

控制器应该是应用程序的最薄部分。控制器的责任是:采取用户输入,并根据该输入改变模型层的状态(在极少数情况下,当前视图)。而已。






使用DI容器



另一种方法是.. ..它基本上是一个复杂的交易(减去一个地方,增加更多的其他)。它还会继承拥有真实 DI容器,而不是像 Pimple 一样的荣耀的服务定位器。



我的建议:查看 Auryn



一个DI容器是什么,使用配置文件或反射,它确定要创建的实例的依赖关系。收集说依赖关系。并传递给实例的构造函数。

  $ request = // ...我们做一些初始化和路由这个
$ resource = $ request-> getParameter('controller');
$ command = $ request-> getMethod()。 $请求 - >的getParameter(动作);

$ container = new DIContainer;
try {
$ controller = $ container-> create($ resource);
$ controller-> {$ command}($ request);
} catch(FubarException $ e){
//做某事,因为请求不存在的东西
}

所以,除了抛出异常的能力之外,控制器的引导保持不变。



另外,在这个你应该已经认识到,从一种方法到另一种方法的切换主要需要完全重写控制器(和相关的单元测试)。



在这种情况下,控制器的方法看起来像:

  private $ authenticationService; 

#IMPORTANT:如果您使用的是基于反射的DI容器,则
#then类型提示将为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()
{
//为seam设置mock

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

$ request = $ this-> getMock('Request',['getParameter']);
$ request-> expects($ this-> exact(2))
- >方法('getParameter')
- >将($ this-> onConsecutiveCalls 'foo','bar'));

//测试本身

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

//完成
}

如你所见,



其他备注






  • 您可能已经注意到,在这两个示例中,您的代码将被耦合到使用的服务名称。即使您使用基于配置的DI容器(可能 symfony ),您仍然会最终定义特定类的名称。


  • DI容器不是魔术



    在过去几年里,DI容器的使用已经有些被夸大了。它不是一个银弹。我甚至会说:DI容器与 SOLID 不兼容。特别是因为它们不能与接口一起使用。您不能真正使用代码中的多态行为,这将由DI容器初始化。



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



    第三个问题是复杂性。良好的DI容器是简单。如果您使用第三方工具,您将会引入额外的风险。


  • 依赖关系太多



    如果你的班级有太多的依赖关系,那么它就是不是 DI的失败。相反,这是一个明确的指示,你的班级做的太多了。违反单一责任原则


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



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



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


  • 它是PHP单元



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


  • 您将进行更多测试



    单元测试示例不是您对控制器方法的整套测试。特别是当你的控制器是不平凡的。




其他资料



...切向科目



支持:无耻的自我宣传




  • 处理类似MVC的架构中的访问控制



    控制器中的一些框架有一个讨厌的习惯,推动授权检查(不要混淆认证不同主题)。除了完全愚蠢的事情外,还会在控制器中引入额外的依赖关系(通常是全局范围的)。



    另有一篇文章使用类似的方法来引入非侵入式日志记录


  • 讲座列表



    这是一个针对想要了解MVC的人,但实际上是OOP和开发实践中的普通教育的材料。这个想法是,当你完成这个列表时,MVC和其他SoC实现只会导致你去哦,这有一个名字,我以为这只是常识。


  • 实施模型图层



    说明上述说明中有什么神奇的服务。



How can I resolve dependencies to a controller that is testable?

How it works: A URI is routed to a Controller, a Controller may have dependencies to perform a certain task.

<?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();

This is my interpretation of the design pattern examples

Registry:

  • Singleton
  • Tight coupling
  • Testable? No

Service Locator

  • Testable? Hard/No (?)

Poor Man Di

  • Testable
  • Hard to maintain with many dependencies

Registry

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

Service Locator

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

How can I resolve dependencies to a controller that is testable?

解决方案

What would be the dependencies that you are talking about in a controller?

The to major solution would be:

  • injecting a factory of services in the controller through constructor
  • using a DI container to pass in the specific services directly

I am going to try to describe both approaches separately in detail.

Note: all examples will be leaving out interaction with view, handling of authorization, dealing with dependencies of service factory and other specifics


Injection of factory

The simplified part of bootstrap stage, which deals with kicking off stuff to the controller, would look kinda like this

$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
}

This approach provides a clear way for extending and/or substituting the model layer related code simply by passing in a different factory as the dependency. In controller it would look something like this:

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 means, that, to test this controller's method, you would have to write a unit-test, which mock the content of $this->serviceFactory, the created instance and the passed in value of $request. Said mock would need to return an instance, which can accept two parameter.

Note: The response to the user should be handled entirely by view instance, since creating the response is part of UI logic. Keep in mind that HTTP Location header is also a form of response.

The unit-test for such controller would look like:

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
}

Controllers are supposed to be the thinnest part of the application. The responsibility of controller is: take user input and, based on that input, alter the state of model layer (and in rare case - current view). That's it.


With DI container

This other approach is .. well .. it's basically a trade of complexity (subtract in one place, add more on others). It also relays on having a real DI containers, instead of glorified service locators, like Pimple.

My recommendation: check out Auryn.

What a DI container does is, using either configuration file or reflection, it determines dependencies for the instance, that you want to create. Collects said dependencies. And passes in the constructor for the instance.

$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
}

So, aside from ability to throw exception, the bootstrapping of the controller stays pretty much the same.

Also, at this point you should already recognize, that switching from one approach to other would mostly require complete rewrite of controller (and the associated unit tests).

The controller's method in this case would look something like:

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')
    );
}

As for writing a test, in this case again all you need to do is provide some mocks for isolation and simply verify. But, in this case, the unit testing is simpler:

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
}

As you can see, in this case you have one less class to mock.

Miscellaneous notes

  • Coupling to the name (in the examples - "authentication"):

    As you might have notices, in both examples your code would be coupled to the name of service, which was used. And even if you use configuration-based DI container (as it is possible in symfony), you still will end up defining name of the specific class.

  • DI containers are not magic:

    The use of DI containers has been somewhat hyped in past couple years. It is not a silver bullet. I would even go as far as to say that: DI containers are incompatible with SOLID. Specifically because they do not work with interfaces. You cannot really use polymorphic behavior in the code, that will be initialized by a DI container.

    Then there is the problem with configuration-based DI. Well .. it's just beautiful while project is tiny. But as project grows, the configuration file grows too. You can end up with glorious WALL of xml/yaml configuration, which is understood by only one single person in project.

    And the third issue is complexity. Good DI containers are not simple to make. And if you use 3rd party tool, you are introducing additional risks.

  • Too many dependencies:

    If your class has too many dependencies, then it is not a failure of DI as practice. Instead it is a clear indication, that your class is doing too many things. It is violating Single Responsibility Principle.

  • Controllers actually have (some) logic:

    The examples used above were extremely simple and where interacting with model layer through a single service. In real world your controller methods will contain control-structures (loops, conditionals, stuff).

    The most basic use-case would be a controller which handles contact form with as "subject" dropdown. Most of the messages would be directed to a service that communicates with some CRM. But if user pick "report a bug", then the message should be passed to a difference service which automatically create a ticket in bug tracker and sends some notifications.

  • It's PHP Unit:

    The examples of unit-tests are written using PHPUnit framework. If you are using some other framework, or writing tests manually, you would have to make some basic alterations

  • You will have more tests:

    The unit-test example are not the entire set of tests that you will have for a controller's method. Especially, when you have controllers that are non-trivial.

Other materials

There are some .. emm ... tangential subjects.

Brace for: shameless self-promotion

  • dealing with access control in MVC-like architecture

    Some frameworks have nasty habit of pushing the authorization checks (do not confuse with "authentication" .. different subject) in the controller. Aside from being completely stupid thing to do, it also introduces additional dependencies (often - globally scoped) in the controllers.

    There is another post which uses similar approach for introducing non-invasive logging

  • list of lectures

    It's kinda aimed at people who want to learn about MVC, but materials there are actually for general education in OOP and development practices. The idea is that, by the time when you are done with that list, MVC and other SoC implementations will only cause you to go "Oh, this had a name? I thought it was just common sense."

  • implementing model layer

    Explains what those magical "services" are in the description above.

这篇关于具有依赖性的可测试控制器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆