嵌套容器的测试和模拟结果


Testing and mocking result of a nested container

我最近接手了一个PHP项目,该项目几乎不包含测试。这个项目相当大,类大多很小,但有一个大问题。

有一个服务定位器巧妙地隐藏在受保护的或私有的变量DI中,所以程序员认为他们做的是正确的,它充当了一个单例,几乎传递给每个类。一个类,该类反过来使用它来检索依赖项。

上周四,我创建了一个由3人组成的新团队,他们的新职责是专注于测试及其自动化。今天,他们中的一人终于向我提出了我担心的问题。一个问题,我不知道答案。

大卫,我该怎么嘲笑这种方法的结果呢隐藏在DI的深处?

重写模块以遵循DI是不可接受的,我们既没有预算也没有时间这样做。

do()方法可以这样调用吗?

class Baz extends AbstractBaz
{
    public function foo()
    {
        $userProcess = $this->DI->Foo->Bar->FooBar->BarFoo->getUserBar();
        $users = $userProcess->do();
        // work with the $users variable
    }
}

定位器本身是巨大的,你可以通过越来越深入地调用数百种方法。

有没有一种方法可以快速模拟Foo->Bar->FooBar->BarFoo->getUserBar的结果?变量可通过魔术__get方法获得,并由@property注释提示。

使用PHPUnit,最好有这样的东西:

$locator = $this
    ->getMockBuilder(''App'DI'Abstracted'DI')
    ->setMethods(['Foo->Bar->FooBar->BarFoo->getUserBar'])
    ->getMock();
$locator
    ->expects($this->any())
    ->method('Foo->Bar->FooBar->BarFoo->getUserBar')
    ->will($this->returnValue($desiredObject));

遗憾的是,这并没有真正奏效。我自己对PHPUnit不是很熟练。有没有我还没有找到的变通办法?

正如我在评论中提到的,您可以存根一个容器,该容器在调用__get方法时返回自身,并最终从您调用的实际方法返回一个模拟对象。使用codeception的Stub组件,这可能看起来像:

$container = Stub::make(
    'Your'Container',
    [
        'getUserBar' => $returnMock,
    ]
);
Stub::update($container, ['__get' => $container]);//return itself on __get access

但正如您所说:这确实表明您试图测试的代码存在更根本的问题。

我早些时候找到了PHPUnit的解决方案,但只是碰巧有时间回答。

在PHPUnit中,使用PHP的Closure来模拟我想要的东西实际上是可能的,这就是PHP中匿名函数的名称。

这里有一个小的PHPUnit工作示例

$classA = $this
    ->getMockBuilder('First'Class'To'Be'Mocked')
    ->disableOriginalConstructor()
    ->setMethods([
        'The',
        'names',
        'of',
        'desired',
        'methods',
    ])
    ->getMock();
$classB = $this
    ->getMockBuilder('Second'Class'To'Be'Mocked')
    ->disableOriginalConstructor()
    ->setMethods([
        'The',
        'names',
        'of',
        'desired',
        'methods',
    ])
    ->getMock();
$serviceLocator = $this
    ->getMockBuilder('Second'Class'To'Be'Mocked')
    ->disableOriginalConstructor()
    ->setMethods([
        '__get',
        'SomeMethod',
    ])
    ->getMock();
$serviceLocator
    ->expects($this->any())
    ->method('__get')
    ->will($this->returnCallback(function($name) use ($serviceLocator, $classA, $classB)
    {
        switch ($name)
        {
            case 'ClassA':
                return $classA;
            case 'ClassB':
                return $classB;
            default:
                return $serviceLocator;
        }
    }));

此代码在执行时将给出所需的返回。

$serviceLocator->This->That->Them->ClassA将返回已定义的$classA变量,将ClassA更改为ClassB会使服务定位器返回$classB变量。

您可以使用回调做任何事情,甚至可以通过引用参数模拟返回值。