9

对于组成另一个对象作为其实现的一部分的对象,编写单元测试以便只测试主要对象的最佳方法是什么?简单的例子:

class myObj { 
    public function doSomethingWhichIsLogged()
    {
        // ...
        $logger = new logger('/tmp/log.txt');
        $logger->info('some message');
        // ...
    }
}

我知道可以设计该对象,以便可以注入记录器对象依赖项并因此在单元测试中进行模拟,但情况并非总是如此 - 在更复杂的场景中,您确实需要组合其他对象或调用静态方法.

由于我们不想测试记录器对象,只测试 myObj,我们如何进行?我们是否使用测试脚本创建了一个存根的“双重”?就像是:

class logger
{
    public function __construct($filepath) {}
    public function info($message) {}
}

class TestMyObj extends PHPUnit_Framework_TestCase 
{
    // ...
}

这对于小对象来说似乎是可行的,但对于 SUT 依赖于返回值的更复杂的 API 来说会很痛苦。另外,如果你想测试对依赖对象的调用,你可以使用模拟对象吗?有没有办法模拟由 SUT 实例化而不是传入的对象?

我已经阅读了关于模拟的手册页,但它似乎没有涵盖依赖组合而不是聚合的这种情况。你怎么做呢?

4

4 回答 4

6

遵循troelskn的建议,这里是您应该做什么的基本示例。

<?php

class MyObj
{
    /**
     * @var LoggerInterface
     */
    protected $_logger;

    public function doSomethingWhichIsLogged()
    {
        // ...
        $this->getLogger()->info('some message');
        // ...
    }

    public function setLogger(LoggerInterface $logger)
    {
        $this->_logger = $logger;
    }

    public function getLogger()
    {
        return $this->_logger;
    }
}


class MyObjText extends PHPUnit_Framework_TestCase
{
    /**
     * @var MyObj
     */
    protected $_myObj;

    public function setUp()
    {
        $this->_myObj = new MyObj;
    }

    public function testDoSomethingWhichIsLogged()
    {
        $mockedMethods = array('info');
        $mock = $this->getMock('LoggerInterface', $mockedMethods);
        $mock->expects($this->any())
             ->method('info')
             ->will($this->returnValue(null));

        $this->_myObj->setLogger($mock);

        // do your testing
    }
}

有关模拟对象的更多信息可以在手册中找到。

于 2009-02-13T12:01:52.887 回答
4

正如您似乎已经意识到的那样,具体类依赖关系使测试变得困难(或完全不可能)。您需要解耦这种依赖关系。一个不会破坏现有 API 的简单更改是默认为当前行为,但提供一个挂钩来覆盖它。有多种方法可以实现这一点。

有些语言有可以将模拟类注入代码的工具,但我不知道 PHP 有这样的东西。在大多数情况下,无论如何重构代码可能会更好。

于 2009-02-13T11:52:33.330 回答
0

看起来我误解了这个问题,让我再试一次:

如果还不算太晚,您应该为记录器使用单例模式或工厂:

class LoggerStub extends Logger {
    public function info() {}
}
Logger::setInstance(new LoggerStub());
...
$logger = Logger::getInstance();

如果您无法更改代码,则可以使用重载__call()的包罗万象的类

class GenericStub {
    public function __call($functionName, $arguments) {}
}
于 2009-02-13T11:58:07.060 回答
0

实际上,构建 PHPUnit 的人发布了一个相当新的 PHP 类重载扩展。它允许您在无法重构代码的情况下覆盖 new 运算符,不幸的是,在 Windows 上安装并不是那么简单。

网址是http://github.com/johannes/php-test-helpers/blob/master/

于 2010-03-05T03:18:56.327 回答