我有一个自定义事件,它有一个公共属性:
class MyCustomEvent
{
public $allowAction = false;
}
我有一个类创建这个事件,并用事件对象分派一个事件,允许事件侦听器/订阅者更改对象的属性。
class MyBizLogic
{
private $dispatcher;
public function __construct(EventDispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
public function doSomething()
{
$event = new MyCustomEvent();
$dispatcher = $this->dispatcher->dispatch('my_custom_event', $event);
if ($event->allowAction) {
// do action
} else {
// do something else
}
}
}
如何对doSomething
进行单元测试?我需要一种方法来控制事件对象的属性,但事件对象不是我可以模拟的依赖项。它是在我测试的方法中创建的。
我不认为这是一种设计气味,因为这是大多数开发人员调度事件的方式。在这里,我可以做些什么来正确地测试doSomething
应该处理的不同结果?
答案:只需创建一个可配置的侦听器,用于测试,并使其在每种情况下都按照您想要的方式运行。
不要那样做这里的设计气味是,事件接收器不应该能够更改发出事件的方法的逻辑。如果有两个听众呢?其中一个可以设定一个值,另一个可以设置另一个值?最后一个会赢,而第一个听众并不知道这一点。
事件是一种通知另一个对象的方式,而发出实体不知道谁将侦听(可能没有其他对象)。发射器应该以相同的方式处理有多少侦听器。如果您需要让其他对象控制逻辑的某些方面,请明确执行(如果有疑问,请写另一个问题,我们将尽力提供帮助)
您无法控制事件对象的属性,因为您正在函数中创建对象。
有几种方法可以解决这个问题。
1) 让您的doSomething方法将MyCustomEvent
对象作为其参数。然后,您就可以传入一个mock对象并以这种方式控制它。
2) 不要在doSomething
中创建事件,而是让调度程序返回一个具有所需属性的MyCustomEvent
。因此,在测试中,您将有一个mockDispatcher
,它将从dispatch
方法返回事件对象。
3) 传入一个事件工厂对象,您可以使用该对象来获取正确事件的实例。然后您可以对此进行模拟,并让它为您返回一个模拟事件对象。
4) 您可以为事件调度程序的dispatch
方法使用回调函数。然后,您的函数可以将MyCustomEvent::$allowAction
属性设置为您想要的值。
$allowAction = 'foo';
$mockEventDispatcher->expects($this->once())
->method('dispatch')
->with('my_custom_event', $this->isInstanceOf('MyCustomEvent')
->will($this->returnCallback(function($string, $event) use ($allowAction) {
$event->allowAction = $allowAction
// Return whatever the dispatcher is supposed to return.
}));
IMO,最后两个选项具有模拟对象返回模拟对象的测试气味,这并不理想。但根据周围的建筑,这可能是你必须走的方向。
创建用于方法的对象总是一种代码味道,这会使测试变得非常困难。大多数事件处理方法都将事件作为参数。