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

Eli*_*iot 4 php database phpunit unit-testing mocking

我一直在研究如何将单元测试覆盖率添加到用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;
}
Run Code Online (Sandbox Code Playgroud)

我和我的同事正在尝试通过PHPUnit实现我们正在编写的一些新类的覆盖.我试图找到一种方法来为我们现有的代码库中的函数创建单独的测试单元测试,类似于上面的伪代码但是不成功.

我在PHPUnit文档中看到的示例都依赖于在类中使用一些方法来附加模拟对象,例如: $objectBeingTested->attach($mockObject);我查看了SimpleUnit,并在那里看到了相同的东西,模拟对象正在通过它的构造函数传递给类.这不会为实例化自己的数据库对象的函数留下太多空间.

有没有办法模拟这些类型的电话?我们可以使用另一个单元测试框架吗?或者我们是否必须改变我们将来使用的模式以便于单元测试?

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

我的团队一直在讨论重构我们在新代码中访问数据库和内存缓存的方法,也许是使用单例.我想如果我们以这样一种方式编写单例,它可以用模拟对象替换它自己的实例,那会有所帮助......

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

谢谢.

edo*_*ian 12

只是添加到@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;
}
Run Code Online (Sandbox Code Playgroud)

有了它,创建模拟对象并将它们传递给代码非常容易.

有几个原因可以解决这个问题(除了创建可测试代码之外).一旦它使你的代码更容易改变(想要不同的db?传递一个不同的db对象而不是改变DatabaseObject中的代码.

这个博客文章告诉你为什么静态方法是坏的,但在你的代码中使用"new"运算符几乎与$x = StaticStuff::getObject();它说的相同,所以它也适用于此.

另一个参考可能是:为什么单例对可测试代码不利,因为它涉及相同的点.

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

像这样的可选依赖注入:

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;
}
Run Code Online (Sandbox Code Playgroud)

或使用"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;
}
Run Code Online (Sandbox Code Playgroud)

额外的东西叫做dependency injection containers允许你把你的所有异议都放在一边,把所有的东西都从容器中取出来,但是因为它让测试变得更难(imho)而且它只会帮助你如果做得很好我不会建议你开始一个但只是使用正常的"依赖注入"来创建可测试的代码.


Ezk*_*zku 7

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

确实如此.您正在描述一种被认为是一种可以避免的编程风格,因为它会导致不可测试的代码.如果您的代码明确依赖于某些外部因素,并且不以任何方式对它们进行抽象,那么您将只能在这些外部性完整的情况下测试该代码.正如你所说,你不能模仿功能为自己创造的东西.

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

也就是说,在没有显式注入的情况下,您可以执行一些操作来启用可模拟性:使用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();
}
Run Code Online (Sandbox Code Playgroud)

现在,如果您正在测试getSomeData(),则可以模拟getMemcache()和getDatabaseObject().下一个重构步骤是将memcache和数据库对象注入到类中,以便它对get_memcache()或DatabaseObject类没有明确的依赖性.这样可以避免在被测单元中对模拟方法的需求.


Dav*_*ess 5

在一个完美的世界中,您将有时间重构所有遗留代码以使用依赖注入或类似的东西.但在现实世界中,你经常需要处理你曾被处理过的手.

PHPUnit的作者Sebastian Bergmann编写了一个测试助手扩展,允许您使用回调和重命名函数覆盖new运算符.这些将允许您在测试期间修改代码,直到您可以重构它以使其更易于测试.当然,使用此编写的测试越多,您将撤消它的工作量就越多.

注: 该试验助手扩展取代由https://github.com/krakjoe/uopz