use*_*056 19 php registry dependencies design-patterns service-locator
如何解析可测试控制器的依赖关系?
工作原理: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();
Run Code Online (Sandbox Code Playgroud)
这是我对设计模式示例的解释
注册地:
服务定位器
穷人迪
注册处
<?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];
}
}
Run Code Online (Sandbox Code Playgroud)
服务定位器
<?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];
}
}
Run Code Online (Sandbox Code Playgroud)
如何解析可测试控制器的依赖关系?
ter*_*ško 19
你在控制器中谈论的依赖是什么?
主要解决方案是:
我将尝试分别详细描述这两种方法.
注意:所有示例都将省略与视图的交互,授权的处理,处理服务工厂的依赖关系以及其他细节
bootstrap阶段的简化部分,处理开启控制器的东西,看起来有点像这样
$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
}
Run Code Online (Sandbox Code Playgroud)
该方法简单地通过将不同的工厂作为依赖性传递来提供用于扩展和/或替换模型层相关代码的清楚方式.在控制器中,它看起来像这样:
public function __construct( $factory )
{
$this->serviceFactory = $factory;
}
public function postLogin( $request )
{
$authentication = $this->serviceFactory->create( 'Authentication' );
$authentication->login(
$request->getParameter('username'),
$request->getParameter('password')
);
}
Run Code Online (Sandbox Code Playgroud)
这意味着,要测试这个控制器的方法,你必须编写一个单元测试,它模拟$this->serviceFactory
创建的实例的内容和传入的值$request
.所述mock需要返回一个实例,它可以接受两个参数.
注意:对用户的响应应完全由视图实例处理,因为创建响应是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
}
Run Code Online (Sandbox Code Playgroud)
控制器应该是应用程序中最薄的部分.控制器的职责是:获取用户输入,并根据该输入改变模型层的状态(在极少数情况下 - 当前视图).而已.
另一种方法是..好吧..它基本上是复杂的交易(在一个地方减去,在其他地方增加更多).它还传递了一个真正的 DI容器,而不是像疙瘩这样的美化服务定位器.
我的建议:看看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
}
Run Code Online (Sandbox Code Playgroud)
因此,除了抛出异常的能力之外,控制器的引导几乎保持不变.
此外,您应该已经认识到,从一种方法切换到另一种方法主要需要完全重写控制器(以及相关的单元测试).
在这种情况下,控制器的方法看起来像:
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')
);
}
Run Code Online (Sandbox Code Playgroud)
至于编写测试,在这种情况下,您需要做的只是提供一些模拟隔离并简单验证.但是,在这种情况下,单元测试更简单:
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
}
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,在这种情况下,您可以减少一个类来进行模拟.
耦合到名称(在示例中 - "身份验证"):
正如您可能已经注意到的那样,在两个示例中,您的代码都将与使用的服务名称相关联.即使你使用基于配置的DI容器(因为它可能在symfony中),你仍然会最终定义特定类的名称.
DI容器不是魔术:
在过去的几年里,DI容器的使用有所夸大.它不是一颗银弹.我甚至会说:DI容器与SOLID不兼容.特别是因为它们不适用于接口.您无法在代码中真正使用多态行为,这将由DI容器初始化.
然后是基于配置的DI的问题.嗯..它很漂亮,而项目很小.但随着项目的增长,配置文件也会增长.您最终可以得到光荣的xml/yaml配置WALL,这只有一个人在项目中可以理解.
第三个问题是复杂性.好的DI容器制作起来并不简单.如果您使用第三方工具,则会引入其他风险.
依赖项太多:
如果你的类有太多的依赖关系,那么它不是 DI作为练习的失败.相反,这是一个明确的迹象,表明你的班级做了太多事情.它违反了单一责任原则.
控制器实际上有(某些)逻辑:
上面使用的示例非常简单,并且通过单个服务与模型层交互.在现实世界中,您的控制器方法将包含控制结构(循环,条件,东西).
最基本的用例是一个控制器,它处理联系表单作为"主题"下拉列表.大多数消息将被定向到与某些CRM通信的服务.但是,如果用户选择"报告错误",则应将该消息传递给差异服务,该服务会自动在错误跟踪器中创建故障单并发送一些通知.
这是PHP单位:
单元测试的示例是使用PHPUnit框架编写的.如果您正在使用其他框架或手动编写测试,则必须进行一些基本的更改
你将有更多的测试:
单元测试示例不是控制器方法的整套测试.特别是,当你有非平凡的控制器.
有一些..嗯...切向主题.
Brace for: shameless self-promotion
一些框架有一个讨厌的习惯,即在控制器中推送授权检查(不要与"身份验证"......不同主题混淆).除了完全愚蠢的事情之外,它还在控制器中引入了额外的依赖关系(通常是全局范围的).
还有另一篇文章使用类似的方法来引入非侵入式伐木
它有点针对那些想要了解MVC的人,但实际上有关于OOP和开发实践的普通教育的材料.我们的想法是,当你完成该列表时,MVC和其他SoC实现只会让你"噢,这有一个名字?我认为这只是常识."
解释上面描述中那些神奇的"服务"是什么.
我从http://culttt.com/2013/07/15/how-to-structure-testable-controllers-in-laravel-4/尝试过这个
你应该如何构建你的控制器以使其可测试。?
测试您的控制器是构建可靠 Web 应用程序的一个关键方面,但重要的是您只测试应用程序的适当部分。
幸运的是,Laravel 4 使分离控制器的关注点变得非常容易。只要您正确构建了它们,这将使测试您的控制器变得非常简单。
我应该在我的控制器中测试什么?
在我开始介绍如何构建控制器以实现可测试性之前,首先重要的是要了解我们究竟需要测试什么。
正如我在设置你的第一个 Laravel 4 控制器中提到的,控制器应该只关心在模型和视图之间移动数据。您不需要验证数据库是否正在提取正确的数据,只需验证控制器是否调用了正确的方法。因此你的控制器测试永远不应该接触数据库。
这就是我今天要向您展示的内容,因为默认情况下很容易将控制器和模型耦合在一起。坏习惯的例子
为了说明我要避免的内容,以下是 Controller 方法的示例:
public function index()
{
return User::all();
}
Run Code Online (Sandbox Code Playgroud)
这是一个不好的做法,因为我们无法模拟User::all();
,因此相关的测试将被迫访问数据库。
依赖注入来拯救
为了解决这个问题,我们必须将依赖注入到控制器中。依赖注入是您向类传递对象实例的地方,而不是让该对象为其自身创建实例。
通过将依赖项注入控制器,我们可以在测试期间向类传递模拟而不是数据库,而不是实际的数据库对象本身。这意味着我们可以在不接触数据库的情况下测试控制器的功能。
作为一般指南,在任何地方看到一个类正在创建另一个对象的实例,这通常表明可以通过依赖注入更好地处理这种情况。您永远不希望您的对象紧密耦合,因此通过不允许一个类实例化另一个类,您可以防止这种情况发生。
自动分辨率
Laravel 4 有一种很好的处理依赖注入的方式。这意味着您可以在许多情况下完全无需任何配置即可解析类。
这意味着如果你通过构造函数向一个类传递另一个类的实例,Laravel 会自动为你注入该依赖项!
基本上,一切都将在您没有任何配置的情况下工作。
将数据库注入控制器
所以现在您了解了问题和解决方案的理论,我们现在可以修复控制器,使其不与数据库耦合。
如果您还记得上周关于 Laravel 存储库的帖子,您可能已经注意到我已经解决了这个问题。
所以,而不是做:
public function index()
{
return User::all();
}
Run Code Online (Sandbox Code Playgroud)
我做了:
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
return $this->user->all();
}
Run Code Online (Sandbox Code Playgroud)
创建 UserController 类时,会自动运行 __construct 方法。__construct 方法注入了 User 存储库的一个实例,然后在类的 $this->user 属性上设置该实例。
现在,只要您想在方法中使用数据库,就可以使用 $this->user 实例。
在 Controller 测试中模拟数据库
当您开始编写 Controller 测试时,真正的魔法就会发生。现在您正在将数据库的实例传递给控制器,您可以模拟数据库而不是实际访问数据库。这不仅会提高性能,而且您在测试后不会有任何测试数据。
我要做的第一件事是在名为功能的测试目录下创建一个新文件夹。我喜欢将控制器测试视为功能测试,因为我们正在测试传入流量和呈现的视图。
接下来,我将创建一个名为 UserControllerTest.php 的文件并编写以下样板代码:
<?php
class UserControllerTest extends TestCase {
}
Run Code Online (Sandbox Code Playgroud)
嘲讽
如果你还记得我的帖子,什么是测试驱动开发?,我谈到了 Mocks 作为依赖对象的替代品。
为了在 Cribbb 中为测试创建 Mocks,我将使用一个名为 Mockery 的奇妙包。
Mockery 允许您模拟项目中的对象,因此您不必使用真正的依赖项。通过模拟一个对象,你可以告诉 Mockery 你想调用哪个方法以及你想返回什么。
这使您能够隔离您的依赖项,因此您只需进行所需的 Controller 调用即可通过测试。
例如,如果你想在你的数据库对象上调用 all() 方法,而不是实际访问数据库,你可以通过告诉 Mockery 你想调用 all() 方法来模拟调用,它应该返回一个预期值。您不是在测试数据库是否可以返回记录,您只关心是否能够触发方法并处理返回值。
安装 Mockery 像所有优秀的 PHP 包一样,Mockery 可以通过 Composer 安装。
要通过 Composer 安装 Mockery,请将以下行添加到您的 composer.json 文件中:
"require-dev": {
"mockery/mockery": "dev-master"
}
Run Code Online (Sandbox Code Playgroud)
接下来,安装软件包:
composer install --dev
Run Code Online (Sandbox Code Playgroud)
设置嘲讽
现在要设置 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;
}
Run Code Online (Sandbox Code Playgroud)
该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();
}
Run Code Online (Sandbox Code Playgroud)
在这个测试中,我要求模拟all()
在UserRepository
. 然后我使用 GET 请求调用页面,然后我断言响应没问题。
结论
测试控制器不应该像人们想象的那样困难或复杂。只要您隔离依赖项并只测试正确的部分,测试控制器就应该非常简单。
可能这对你有帮助。
归档时间: |
|
查看次数: |
2386 次 |
最近记录: |