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

查看:31
本文介绍了如何在 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.它是硬编码的,它是一个单例.后者意味着防止先前测试的任何副作用.必须为您将在 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' 对象.因为你喜欢 Registry,你的 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 的调用.你的测试必须确保它包含一些东西,否则测试将失败.当然,我们的数据库类也是单例,需要在测试之间重新设置.呸!

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

并意识到您还有另一个依赖项:您的物理测试数据库尚未设置.当你设置测试数据库并填充数据时,你的老板走过来告诉你你要去SOA 现在所有这些数据库调用都必须替换为 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 服务创建一个双精度值,该服务期望在测试期间使用的第一个参数被调用一次method 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.

将其放入测试用例的方法中后,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);
}

不要忘记相应地更改模拟

Don't forget to change the mock accordingly

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

和 setup() 变成

and setup() becomes

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

瞧!又好又容易.我们现在可以专注于测试 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.

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

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天全站免登陆