简体   繁体   English

在PHP中使用模拟对象,在实例化自己的对象的函数中

[英]Using mock objects in PHP inside functions that instantiate their own objects

I have been looking into how to add unit testing coverage to a large, existing codebase written in PHP. 我一直在研究如何将单元测试覆盖率添加到用PHP编写的大型现有代码库中。 Many functions in both static and instantiable classes make a call to a library or instantiate an object in order to obtain connections to memcache and the database. 静态和可实例化类中的许多函数都会调用库或实例化对象,以获取与memcache和数据库的连接。 They typically look something like this: 它们通常看起来像这样:

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = get_memcache();

    $results = $cache->get($key);
    if (!$results) {
        $database = new DatabaseObject();
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

My colleagues and I are currently trying to implement coverage via PHPUnit for a few of the new classes we are writing. 我和我的同事正在尝试通过PHPUnit实现我们正在编写的一些新类的覆盖。 I have attempted to find a way to create unit tests in an isolated manner for functions in our existing codebase that resemble the pseudo-code above, but have been unsuccessful. 我试图找到一种方法来为我们现有的代码库中的函数创建单独的测试单元测试,类似于上面的伪代码但是不成功。

The examples I've seen in the PHPUnit documentation all rely on having some method in the class by which a mock object can be attached to it, such as: $objectBeingTested->attach($mockObject); 我在PHPUnit文档中看到的示例都依赖于在类中有一些方法可以将模拟对象附加到它上面,例如: $objectBeingTested->attach($mockObject); I looked at SimpleUnit, and saw the same thing there, the mock objects were being passed into the class via its constructor. 我看了SimpleUnit,看到了同样的东西,模拟对象通过它的构造函数传递给了类。 This doesn't leave much room for functions which instantiate their own database objects. 这不会为实例化自己的数据库对象的函数留下太多空间。

Is there any way to mock out these sorts of calls? 有没有办法模拟这些类型的电话? Is there another unit testing framework we can use? 我们可以使用另一个单元测试框架吗? Or are we going to have to change the patterns we are using in the future in order to facilitate unit testing? 或者我们是否必须改变我们将来使用的模式以便于单元测试?

What I'd like to do is be able to swap out an entire class with a mock class when running tests. 我想做的是在运行测试时能够用模拟类替换整个类。 For instance, the DatabaseObject class could be replaced with a mock class, and any time it's instantiated during a test, it would actually be an instance of the mock version. 例如,DatabaseObject类可以替换为mock类,并且在测试期间实例化它时,它实际上是模拟版本的实例。

There has been talk in my team of refactoring our methods of accessing the database and memcache in new code, perhaps using singletons. 我的团队一直在讨论重构我们在新代码中访问数据库和内存缓存的方法,也许是使用单例。 I suppose that could help if we were to write the singleton in such a way that its own instance of itself could be replaced with a mock object... 我想如果我们以这样一种方式编写单例,它可以用模拟对象替换它自己的实例,那会有所帮助......

This is my first foray into unit testing. 这是我第一次涉足单元测试。 If I'm doing it wrong, please say so. 如果我做错了,请说出来。 :) :)

Thanks. 谢谢。

Just to add on to @Ezku answer (+1, all what i would have said too) to final code could look something like this (using Dependency injection ) 只是添加到@Ezku的答案(+1,我会说的所有内容)到最终代码可能看起来像这样(使用依赖注入

public function __construct(Memcached $mem, DatabaseObject $db) {
    $this->mem = $mem;
    $this->db = $db;
}

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = $this->mem;

    $results = $cache->get($key);
    if (!$results) {
        $database = $this->db;
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

With that it is really easy to create the mock objects and pass them into the code. 有了它,创建模拟对象并将它们传递给代码非常容易。

There are several reasons why you could want to do this (apart from creating testable code). 有几个原因可以解决这个问题(除了创建可测试代码之外)。 For once it makes your code much more open to change (want different db? pass in a different db object instead of changeing the code in your DatabaseObject. 一旦它使你的代码更容易改变(想要不同的db?传递一个不同的db对象而不是改变DatabaseObject中的代码。

This Blog post tells you about why static methods are bad but using the "new" operator in your code is pretty much the same thing than saying $x = StaticStuff::getObject(); 这篇博客文章告诉你为什么静态方法不好但在你的代码中使用“new”运算符几乎与说$x = StaticStuff::getObject(); so it applies here too. 所以它也适用于此。

Another reference can be: Why singletons are bad for testable code because it touches on the same points. 另一个参考可能是: 为什么单例对可测试代码不利,因为它涉及相同的点。

If you already have some more code written there are some ways to work those idea in without changeing everything at once. 如果您已经编写了更多代码,那么有一些方法可以在不改变所有内容的情况下处理这些想法。

Optional dependency injection like this: 像这样的可选依赖注入:

public function __construct(Memcached $mem = null, DatabaseObject $db = null) {
    if($mem === null) { $mem = new DefaultCacheStuff(); }
    if($db === null) { $db = new DefaultDbStuff(); }
    $this->mem = $mem;
    $this->db = $db;
}

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = $this->mem;

    $results = $cache->get($key);
    if (!$results) {
        $database = $this->db;
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

or using "setter injection": 或使用“setter injection”:

public function __construct(Memcached $mem = null, DatabaseObject $db = null) {
    $this->mem = new DefaultCacheStuff();
    $this->db = new DefaultDbStuff();
}

public function setDatabaseObject(DatabaseObject $db) { 
    $this->db = $db;
}

public function setDatabaseObject(Memcached $mem) { 
    $this->mem = $mem;
}

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = $this->mem;

    $results = $cache->get($key);
    if (!$results) {
        $database = $this->db;
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

Additional there are things called dependency injection containers that allow you to put all your objection creating away and pull everything out of that container, but since it makes testing a bit harder (imho) and it only helps you if done really well i wouldn't suggest starting with one but just using normal "dependency injection" to create testable code. 另外还有一些称为dependency injection containers东西,它允许你把你所有的异议创建出来并从容器中取出所有东西,但是因为它使得测试更加困难(imho)并且它只会帮助你如果做得很好我不会建议从一个开始,但只使用正常的“依赖注入”来创建可测试的代码。

This doesn't leave much room for functions which instantiate their own database objects. 这不会为实例化自己的数据库对象的函数留下太多空间。

Precisely so. 确实如此。 You're describing a style of programming that is considered one to avoid precisely because it leads into untestable code. 您正在描述一种被认为是一种可以避免的编程风格,因为它会导致不可测试的代码。 If your code explicitly depends on some externalities and does not in any way abstract over them, you're only going to be able to test that code with those externalities intact. 如果您的代码明确依赖于某些外部因素,并且不以任何方式对它们进行抽象,那么您将只能在这些外部性完整的情况下测试该代码。 As you say, you can't mock things that functions create for themselves. 正如你所说,你不能模仿功能为自己创造的东西。

To make your code testable, it's preferable to apply dependency injection: pass the dependencies you wish to be mockable into the unit's context from the outside. 为了使您的代码可测试,最好应用依赖注入:将您希望可模拟的依赖项从外部传递到单元的上下文中。 This is usually seen as resulting in better class design in the first place. 这通常被视为首先导致更好的班级设计。

That said, there are some things you can do to enable mockability without explicit injection: using PHPUnit's mock object facilities, you can override methods even in the unit under test. 也就是说,在没有显式注入的情况下,您可以执行一些操作来启用可模拟性:使用PHPUnit的模拟对象工具,即使在被测单元中也可以覆盖方法。 Consider a refactoring like this. 考虑像这样的重构。

public function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = $this->getMemcache();

    $results = $cache->get($key);
    if (!$results) {
        $database = $this->getDatabaseObject();
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

public function getMemcache() {
    return get_memcache();
}

public function getDatabaseObject() {
    return new DatabaseObject();
}

Now, if you're testing getSomeData(), you can mock out getMemcache() and getDatabaseObject(). 现在,如果您正在测试getSomeData(),则可以模拟getMemcache()和getDatabaseObject()。 The next refactoring step would be to inject the memcache and database objects into the class so that it would have no explicit dependencies on get_memcache() or the DatabaseObject class. 下一个重构步骤是将memcache和数据库对象注入到类中,以便它对get_memcache()或DatabaseObject类没有明确的依赖性。 This would obviate the need for mocking methods in the unit under test itself. 这样可以避免在被测单元中对模拟方法的需求。

In a perfect world, you'd have the time to refactor all your legacy code to use dependency injection or something similar. 在一个完美的世界中,您将有时间重构所有遗留代码以使用依赖注入或类似的东西。 But in the real world, you often have to deal the hand you've been dealt. 但在现实世界中,你经常需要处理你曾被处理过的手。

Sebastian Bergmann, the author of PHPUnit, wrote a test helpers extension that allows you to override the new operator with a callback and rename functions. PHPUnit的作者Sebastian Bergmann编写了一个测试助手扩展 ,允许您使用回调和重命名函数覆盖new运算符。 These will allow you to monkey patch your code during testing until you can refactor it to be more testable. 这些将允许您在测试期间修改代码,直到您可以重构它以使其更易于测试。 Granted, the more tests you write using this, the more work you'll have undoing it. 当然,使用此编写的测试越多,您将撤消它的工作量就越多。

Note: the Test-Helper extension is superseded by https://github.com/krakjoe/uopz 注意: Test-Helper扩展已被 https://github.com/krakjoe/uopz 取代

I would suggest a very simple dependency injector. 我建议一个非常简单的依赖注入器。 They can be very very easy to use for new functions inside legacy code. 它们可以非常容易地用于遗留代码中的新功能。 Also you can easy refactore such code as you posted. 您也可以轻松重构您发布的代码。

I suggest a simple one like I recently developed for a similar occasion: https://packagist.org/packages/tflori/dependency-injector 我建议像我最近为类似场合开发的一个简单的: https//packagist.org/packages/tflori/dependency-injector

In some bootstrap file or configuration file you write something like this: 在某些引导程序文件或配置文件中,您可以编写如下内容:

<?php

DI::set('database', function() { return new DatabaseObject(); });
DI::set('memcache', function() { return get_memcache(); });

And then your function can look like this: 然后你的函数看起来像这样:

<?php

function getSomeData() {
    $key = "SomeMemcacheKey";
    $cache = DI::get('memcache');

    $results = $cache->get($key);
    if (!$results) {
        $database = DI::get('database');
        $sql = "SELECT * from someDatabase.someTable";
        $results = $database->query($sql);

        $cache->set($key, $results);
    }

    return $results;
}

To test the code you can write a testClass like this: 要测试代码,您可以像这样编写一个testClass:

<?php

use PHPUnit\Framework\TestCase;

class GetSomeDataTest extends TestCase {
    public function tearDown() {
        Mockery::close();
        parent::tearDown();
    }

    public function testReturnsCached() {
        $mock = Mockery::mock('memcache_class');
        $mock->shouldReceive('get')->once()->with('SomeMemcacheKey')->andReturn('anyResult');
        DI::set('memcache', $mock);

        $result = getSomeData();

        $this->assertSame('anyResult', $result);
    }

    public function testQueriesDatabase() {
        $memcache = Mockery::mock('memcache_class');
        $memcache->shouldReceive('get')->andReturn(null);
        $memcache->shouldIgnoreMissing();
        DI::set('memcache', $memcache);

        $database = Mockery::mock(DatabaseObject::class);
        $database->shouldReceive('query')->once()->andReturn('fooBar');
        DI::set('database', $database);

        $result = getSomeData();

        $this->assertSame('fooBar', $result);
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM