php - 在实例化自己对象的函数内部使用 PHP 中的模拟对象

标签 php database unit-testing mocking phpunit

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

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

我和我的同事目前正在尝试通过 PHPUnit 为我们正在编写的一些新类实现覆盖。我试图找到一种方法,以隔离的方式为我们现有代码库中类似于上述伪代码的函数创建单元测试,但没有成功。

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

有没有办法模拟这些类型的调用?我们可以使用另一个单元测试框架吗?或者我们是否必须更改我们在未来使用的模式以促进单元测试?

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

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

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

谢谢。

最佳答案

只是添加到@Ezku 的答案(+1,我也会说的所有内容)到最终代码可能看起来像这样(使用 Dependency injection )

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

有了它,创建模拟对象并将它们传递到代码中真的很容易。

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

This Blog post告诉您为什么静态方法不好,但是在您的代码中使用“new”运算符与说 $x = StaticStuff::getObject(); 几乎是一回事,所以它也适用于此。

另一个引用可以是:Why singletons are bad for testable code因为它涉及相同的点。

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

像这样的可选依赖注入(inject):

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

或使用“setter 注入(inject)”:

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

另外还有一些叫做 dependency injection containers 的东西可以让你把所有的反对意见都扔掉并将所有东西从那个容器中拉出来,但是因为它让测试有点困难(恕我直言)而且它只如果做得很好,对你有帮助我不建议从一个开始,而只是使用普通的“依赖注入(inject)”来创建可测试的代码。

关于php - 在实例化自己对象的函数内部使用 PHP 中的模拟对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5281418/

相关文章:

database - 如何在 Android 虚拟设备上找到数据库文件?

c# - 集成测试导致 Entity Framework 超时

php - 如何在查询字符串中传递数组?

php - Laravel 5 背景图像没有出现在 DOMPDF 上?

MySQL查询从不同列中查找数据并在线离线查看

rdbms - 具有聚簇索引的表的记录是否按聚簇键物理排序?

python - 模拟 Django 模型和 save()

Android studio 1.1.0 无法设置 Robolectric

javascript - 根据在此子类别中选择的选项动态更改子类别选择选项

phpmailer 交换服务器身份验证