如何在PHP中很难测试注册表模式或单例? [英] How is testing the registry pattern or singleton hard in PHP?

查看:86
本文介绍了如何在PHP中很难测试注册表模式或单例?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

为什么要使用请求驱动的PHP之类的语言来严格测试单个或注册表模式?

Why is testing singletons or registry pattern hard in a language like PHP which is request driven?

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

You can write and run tests aside from the actual program execution, so that you are free to affect the global state of the program and run some tear downs and initialization per each test function to get it to the same state for each test.

我想念什么吗?

推荐答案

确实可以在实际程序执行之外编写和运行测试,以便您可以自由地影响程序的全局状态并对每个测试功能进行一些拆卸和初始化,以使其在每个测试中都处于相同的状态." ,这样做很乏味.您想单独测试TestSubject,而不要花时间重新创建工作环境.

While it's true that "you can write and run tests aside of the actual program execution so that you are free to affect the global state of the program and run some tear downs and initialization per each test function to get it to the same state for each test.", it is tedious to do so. You want to test the TestSubject in isolation and not spend time recreating a working environment.

class MyTestSubject
{
    protected $registry;

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

要使其正常工作,您必须具有具体的Registry.它是硬编码的,是一个Singleton.后者是指防止先前测试产生的任何副作用.必须为将在MyTestSubject上运行的每个测试重置它.您可以添加Registry::reset()方法并在setup()中调用它,但是添加仅用于测试的方法似乎很难.假设您仍然需要此方法,那么最终得到

To get this working you have to have the concrete Registry. It's hardcoded, and it's a Singleton. The latter means to prevent any side-effects from a previous test. It has to be reset for each test you will run on MyTestSubject. You could add a Registry::reset() method and call that in setup(), but adding a method just for being able to test seems ugly. Let's assume you need this method anyway, so you end up with

public function setup()
{
    Registry::reset();
    $this->testSubject = new MyTestSubject;
}

现在您仍然没有应该在foo中返回的'MyActiveRecord'对象.因为您喜欢注册表,所以您的MyActiveRecord实际上看起来像这样

Now you still don't have the 'MyActiveRecord' object it is supposed to return in foo. Because you like Registry, your MyActiveRecord actually looks like this

class MyActiveRecord
{
    protected $db;

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

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

There is another call to Registry in the constructor of MyActiveRecord. You test has to make sure it contains something, otherwise the test will fail. Of course, our database class is a Singleton as well and needs to be reset between tests. Doh!

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

因此,在完成这些设置之后,您就可以进行测试了

So with those finally set up, you can do your test

public function testFooDoesSomethingToQueryResults()
{
    $this->assertSame('expectedResult', $this->testSubject->findById(1));
}

并意识到您还有另一个依赖项:您的物理测试数据库尚未设置.当您设置测试数据库并填充数据时,您的老板来告诉您您要 Web服务调用

and realize you have yet another dependency: your physical test database wasn't setup yet. While you were setting up the test database and filled it with data, your boss came along and told you that you are going SOA now and all these database calls have to be replaced with Web service calls.

为此提供了一个新的类MyWebService,您必须使MyActiveRecord代替使用它.太好了,正是您所需要的.现在,您必须更改所有使用该数据库的测试.该死,你想.只是为了确保doSomethingWithResults正常工作而已? MyTestSubject并不真正在乎数据来自何处.

There is a new class MyWebService for that, and you have to make MyActiveRecord use that instead. Great, just what you needed. Now you have to change all the tests that use the database. Dammit, you think. All that crap just to make sure that doSomethingWithResults works as expected? MyTestSubject doesn't really care where the data comes from.

好消息是,您确实可以通过存根或模拟来替换所有依赖项.两次考试将假装成真.

The good news is, you can indeed replace all the dependencies by stubbing or mock them. A test double will pretend to be the real thing.

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

这将为Web服务创建一个双重名称,它在测试期间将一次称为一次,并且将的第一个参数方法 findById为1.它返回预定义的数据.

This will create a double for a Web service that expects to be called once during the test with the first argument to method findById being 1. It will return predefined data.

将其放入TestCase中的方法后,您的setup变为

After you put that in a method in your TestCase, your setup becomes

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

太好了.您现在不必再为设置实际环境而烦恼了.好吧,除了注册表.怎么样呢?但是如何做到这一点.它是硬编码的,因此无法在测试运行时替换.废话!

Great. You no longer have to bother about setting up a real environment now. Well, except for the Registry. How about mocking that too. But how to do that. It's hardcoded so there is no way to replace at test runtime. Crap!

但是请稍等,我们不是只是说MyTestClass不在乎数据来自何处?是的,它只是在乎它可以调用findById方法.您希望现在想一想:为什么注册表在那里存在?是的,你是.让我们将整个内容更改为

But wait a second, didn't we just say MyTestClass doesn't care where the data comes from? Yes, it just cares that it can call the findById method. You hopefully think now: why is the Registry in there at all? And right you are. Let's change the whole thing to

class MyTestSubject
{
    protected $finder;

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

再见注册表.我们现在正在注入依赖项MyWebSe…err…Finder ?!是的.我们只关心方法findById,所以我们现在正在使用接口

Byebye Registry. We are now injecting the dependency MyWebSe… err… Finder?! Yeah. We just care about the method findById, so we are using an interface now

interface Finder
{
    public function findById($id);
}

别忘了相应地更改模拟

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

和setup()变为

public function setup()
{
    $this->testSubject = new MyTestSubject($this->getFinderMock());
}

Voila!尼斯和容易和.我们现在可以集中精力测试MyTestClass.

Voila! Nice and easy and. We can concentrate on testing MyTestClass now.

当您这样做时,您的老板再次打来电话,说他希望您切换回数据库,因为SOA实际上只是价格过高的顾问用来使您感到有企业精神的流行语.但这一次您不必担心,因为您不必再​​次更改测试.它们不再依赖于环境.

While you were doing that, your boss called again and said he wants you to switch back to a database because SOA is really just a buzzword used by overpriced consultants to make you feel enterprisey. This time you don't worry though, because you don't have to change your tests again. They no longer depend on the environment.

当然,您仍然必须确保MyWebservice和MyActiveRecord都为您的实际代码实现了Finder接口,但是由于我们假定它们已经具有这些方法,因此只需在类上打implements Finder

Of course, you still you have to make sure that both MyWebservice and MyActiveRecord implement the Finder interface for your actual code, but since we assumed them to already have these methods, it's just a matter of slapping implements Finder on the class.

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

And that's it. Hope that helped.

在测试Singleton和处理全局状态时,您可以找到有关其他缺点的更多信息

You can find additional information about other drawbacks when testing Singletons and dealing with global state in

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

This should be of most interest, because it is by the author of PHPUnit and explains the difficulties with actual examples in PHPUnit.

感兴趣的还有:

这篇关于如何在PHP中很难测试注册表模式或单例?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆