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

Testing and mocking result of a nested container

我最近接手了一个 PHP 项目,其中几乎没有测试。这个项目比较大,class大部分都很小,但是有一个大问题。

有一个服务定位器巧妙地隐藏在受保护或私有变量中 DI,因此程序员认为他们在做正确的事情,它充当单例并且几乎传递给每个单例 class. class,在 return 中使用它来检索依赖项。

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

David, how am I suppose to mock the result of the method which is hidden deeply within the 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 方法时 returns 本身,并且 returns 来自你最后调用的实际方法的模拟对象。使用 codeception's Stub component,这可能类似于:

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

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

我之前找到了PHP单元的解决方案,只是刚好有时间回答。

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

这是一个小工作PHP单元示例

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

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

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

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