如何在PHP中测试注册表模式或单例?

Tow*_*wer 14 php testing singleton static

为什么在像请求驱动的PHP这样的语言中难以测试单例或注册表模式?

除了实际的程序执行之外,您可以编写和运行测试,这样您就可以自由地影响程序的全局状态,并为每个测试函数运行一些拆卸和初始化,以使每个测试的状态达到相同状态.

我错过了什么吗?

Gor*_*don 35

虽然这是真的,"你可以编写和运行测试,撇开实际的程序执行的,这样你可以自由地影响程序的全局状态和运行每个测试功能有些撕裂起伏和初始化得到它的每一个相同的状态测试." 这样做很乏味.您希望单独测试TestSubject,而不是花时间重新创建工作环境.

class MyTestSubject
{
    protected $registry;

    public function __construct()
    {
        $this->registry = Registry::getInstance();
    }
    public function foo($id)
    {
        return $this->doSomethingWithResults(
            $registry->get('MyActiveRecord')->findById($id)
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

为了实现这个目标,你必须具备这个功能Registry.它是硬编码的,它是一个单身人士.后者意味着防止先前测试的任何副作用.必须为将在MyTestSubject上运行的每个测试重置它.你可以添加一个Registry::reset()方法并调用它setup(),但添加一个方法只是为了能够测试看起来很难看.让我们假设你无论如何都需要这种方法,所以你最终得到了

public function setup()
{
    Registry::reset();
    $this->testSubject = new MyTestSubject;
}
Run Code Online (Sandbox Code Playgroud)

现在你还没有它应该返回的'MyActiveRecord'对象foo.因为你喜欢Registry,你的MyActiveRecord实际上就是这样的

class MyActiveRecord
{
    protected $db;

    public function __construct()
    {
        $registry = Registry::getInstance();
        $this->db = $registry->get('db');
    }
    public function findById($id) { … }
}
Run Code Online (Sandbox Code Playgroud)

在MyActiveRecord的构造函数中还有另一个对Registry的调用.您测试必须确保它包含某些内容,否则测试将失败.当然,我们的数据库类也是Singleton,需要在测试之间重置.卫生署!

public function setup()
{
    Registry::reset();
    Db::reset();
    Registry::set('db', Db::getInstance('host', 'user', 'pass', 'db'));
    Registry::set('MyActiveRecord', new MyActiveRecord);
    $this->testSubject = new MyTestSubject;
}
Run Code Online (Sandbox Code Playgroud)

所以最终设置的那些,你可以做你的测试

public function testFooDoesSomethingToQueryResults()
{
    $this->assertSame('expectedResult', $this->testSubject->findById(1));
}
Run Code Online (Sandbox Code Playgroud)

并意识到你还有另一个依赖:你的物理测试数据库尚未设置.当您设置测试数据库并填充数据时,您的老板出现并告诉您现在正在进行SOA,并且所有这些数据库调用都必须替换为Web服务调用.

有一个新的类MyWebService,你必须使MyActiveRecord使用它.很棒,正是你需要的.现在您必须更改使用数据库的所有测试.你想,该死的.所有这些废话只是为了确保doSomethingWithResults按预期工作?MyTestSubject并不关心数据的来源.

介绍嘲笑

好消息是,您确实可以通过存根或模拟它们来替换所有依赖项.测试双人将假装是真实的.

$mock = $this->getMock('MyWebservice');
$mock->expects($this->once())
     ->method('findById')
     ->with($this->equalTo(1))
     ->will($this->returnValue('Expected Unprocessed Data'));
Run Code Online (Sandbox Code Playgroud)

这将创建一个双为Web服务预计被称为一次试验期间第一个参数的方法 findById是1. 返回预定义的数据.

将它放入TestCase中的方法之后,setup就变成了

public function setup()
{
    Registry::reset();
    Registry::set('MyWebservice', $this->getWebserviceMock());
    $this->testSubject = new MyTestSubject;
}
Run Code Online (Sandbox Code Playgroud)

大.您现在不再需要为创建真实环境而烦恼.好吧,除了注册表.怎么样嘲笑那个.但是如何做到这一点.它是硬编码的,因此在测试运行时无法替换.扯淡!

但等一下,我们不是只说MyTestClass不关心数据来自何处?是的,它只关心它可以调用该findById方法.你希望现在想一想:为什么注册表会在那里?对,你是对的.让我们改变一切

class MyTestSubject
{
    protected $finder;

    public function __construct(Finder $finder)
    {
        $this->finder = $finder;
    }
    public function foo($id)
    {
        return $this->doSomethingWithResults(
            $this->finder->findById($id)
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

Byebye Registry.我们现在正在注入依赖MyWebSe ...错误... Finder ?! 是啊.我们只关心这个方法findById,所以我们现在正在使用一个接口

interface Finder
{
    public function findById($id);
}
Run Code Online (Sandbox Code Playgroud)

不要忘记相应地更改模拟

$mock = $this->getMock('Finder');
$mock->expects($this->once())
     ->method('findById')
     ->with($this->equalTo(1))
     ->will($this->returnValue('Expected Unprocessed Data'));
Run Code Online (Sandbox Code Playgroud)

和setup()成为

public function setup()
{
    $this->testSubject = new MyTestSubject($this->getFinderMock());
}
Run Code Online (Sandbox Code Playgroud)

瞧!很好,很容易.我们现在可以集中精力测试MyTestClass.

当你这样做的时候,你的老板再次打电话说他希望你切换回数据库,因为SOA实际上只是价格过高的顾问使用的流行语,让你觉得自己很有事.这次你不用担心,因为你不必再次改变你的测试.他们不再依赖环境.

当然,你还是你必须确保两个为MyWebService和MyActiveRecord实施实际代码查找器接口,但由于我们假定他们已经有了这些方法,它只是打耳光的事implements Finder上的类.

就是这样.希望有所帮助.

其他资源:

您可以在测试单身人士和处理全球状态时找到有关其他缺点的其他信息

这应该是最感兴趣的,因为它是PHPUnit的作者,并解释了PHPUnit中实际示例的困难.

同样感兴趣的是:


Gor*_*onM 5

单例(在所有OOP语言中,而不仅仅是PHP)使得一种称为单元测试的特殊调试很困难,原因与全局变量相同.他们将全局状态引入程序,这意味着您无法单独测试依赖于单例的软件模块.单元测试应该只包括被测代码(及其超类).

单身人士本质上是全球性的国家,虽然在某些情况下拥有全球国家可能有意义,但除非必要,否则应该避免.