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

在PHP函数中使用模拟对象来实例化自己的对象,php,database,unit-testing,mocking,phpunit,Php,Database,Unit Testing,Mocking,Phpunit,我一直在研究如何将单元测试覆盖率添加到用PHP编写的大型现有代码库中。静态类和可实例化类中的许多函数都会调用库或实例化对象,以获得到memcache和数据库的连接。它们通常看起来像这样: public function getSomeData() { $key = "SomeMemcacheKey"; $cache = get_memcache(); $results = $cache->get($key); if (!$results) {

我一直在研究如何将单元测试覆盖率添加到用PHP编写的大型现有代码库中。静态类和可实例化类中的许多函数都会调用库或实例化对象,以获得到memcache和数据库的连接。它们通常看起来像这样:

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

DI::set('database', function() { return new DatabaseObject(); });
DI::set('memcache', function() { return get_memcache(); });
<?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;
}
<?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);
    }
}
我的同事和我目前正试图通过PHPUnit为我们正在编写的几个新类实现覆盖。我试图找到一种方法,以一种孤立的方式为我们现有的代码库中类似于上面的伪代码的函数创建单元测试,但没有成功

我在PHPUnit文档中看到的示例都依赖于类中的某些方法,通过这些方法可以将模拟对象附加到类中,例如:
$objectBeingTested->attach($mockObject)我看了SimpleUnit,看到了同样的情况,模拟对象通过其构造函数传递到类中。这不会为实例化自己的数据库对象的函数留下太多空间

有没有办法模拟这种电话?我们是否可以使用另一个单元测试框架?或者,为了便于单元测试,我们是否必须改变我们在未来使用的模式

我想做的是在运行测试时能够用模拟类替换整个类。例如,DatabaseObject类可以替换为一个模拟类,并且在测试期间任何时候实例化它时,它实际上都是模拟版本的实例

在我的团队中,有人讨论过重构我们在新代码中访问数据库和memcache的方法,也许是使用单例。我想,如果我们以这样一种方式来编写singleton,它自己的实例可以被一个模拟对象替换,这可能会有所帮助

这是我第一次涉足单元测试。如果我做错了,请说出来。:)

谢谢

这不会为实例化自己的数据库对象的函数留下太多空间

确实如此。您所描述的编程风格被认为是需要避免的,因为它会导致不稳定的代码。如果您的代码显式地依赖于某些外部性,并且没有以任何方式对它们进行抽象,那么您只能在这些外部性完好无损的情况下测试代码。正如你所说,你不能嘲笑函数为自己创建的东西

为了使代码可测试,最好应用依赖项注入:从外部将希望可模拟的依赖项传递到单元的上下文中。这通常被视为首先导致更好的类设计

也就是说,您可以做一些事情来实现无显式注入的可模拟性:使用PHPUnit的模拟对象工具,您甚至可以覆盖被测试单元中的方法。考虑这样的重构。

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

现在,如果您正在测试getSomeData(),那么可以模拟getMemcache()和getDatabaseObject()。下一个重构步骤是将memcache和数据库对象注入到类中,这样它就不会对get_memcache()或DatabaseObject类有显式依赖关系。这将避免在被测单元本身中使用模拟方法。

只需在@Ezku answer(+1,我也会这么说)的最终代码中添加类似的内容(使用)

这样,创建模拟对象并将其传递到代码中就非常容易了

除了创建可测试代码之外,您可能希望这样做的原因有很多。这一次,它使您的代码更易于更改(想要不同的db?传入不同的db对象,而不是更改数据库对象中的代码)

告诉您静态方法不好的原因,但是在代码中使用“new”操作符与说
$x=StaticStuff::getObject();
几乎是一样的,所以它也适用于这里

另一个参考可以是:因为它涉及相同的点

如果您已经编写了更多的代码,那么有一些方法可以在不立即更改所有内容的情况下实现这些想法

可选的依赖项注入,如下所示:

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

DI::set('database', function() { return new DatabaseObject(); });
DI::set('memcache', function() { return get_memcache(); });
<?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;
}
<?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);
    }
}
或使用“setter注入”:


另外,还有一些称为
依赖注入容器的东西,允许您将所有创建的对象放在一边,并从容器中取出所有内容,但由于这会使测试变得更困难(imho),并且只有在做得非常好时才有帮助,我不建议从一个容器开始,而只是使用普通容器“依赖注入”来创建可测试的代码。

在一个完美的世界里,你会有时间重构你所有的遗留代码来使用依赖注入或类似的东西。但在现实世界中,你经常不得不处理你已经处理过的事情

PHPUnit的作者塞巴斯蒂安·伯格曼(Sebastian Bergmann)编写了一个函数,允许您使用回调和重命名函数覆盖新的运算符。这将允许您在测试过程中对代码进行修补,直到您可以将其重构为更易测试的代码。当然,您使用此函数编写的测试越多,撤销它的工作就越多

注意:


我建议使用一个非常简单的依赖项注入器。它们可以非常容易地用于遗留代码中的新函数。此外,您还可以轻松地重构您发布的代码

我建议使用一个类似于我最近为类似场合开发的简单方法:

在某些引导文件或配置文件中,您可以编写如下内容:

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

DI::set('database', function() { return new DatabaseObject(); });
DI::set('memcache', function() { return get_memcache(); });
<?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;
}
<?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);
    }
}

似乎没有办法,因为关系很紧。建议去看Symfony DI.+1。但是,如果你真的知道自己在做什么,而且这些选项根本不可行,你会同意这只能作为最后手段吗?@edorian-绝对同意。但是当你不得不处理无法控制的遗留代码时,或者当你的老板认为依赖注入是可行的时候邪恶,知道是好事