在PHPUnit中测试具有依赖项的对象

时间:2022-10-15 20:05:36

For objects which compose another object as part of their implementation, what's the best way to write the unit test so only the principle object gets tested? Trivial example:

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

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

I know that the object could be designed so that the logger object dependency could be injected and hence mocked in a unit test, but that's not always the case - in more complicated scenarios, you do need to compose other objects or make calls to static methods.

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

As we don't want to test the logger object, only the myObj, how do we proceed? Do we create a stubbed "double" with the test script? Something like:

由于我们不想测试logger对象,只有myObj,我们如何进行?我们用测试脚本创建一个stubbed“double”吗?就像是:

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

class TestMyObj extends PHPUnit_Framework_TestCase 
{
    // ...
}

This seems feasible for small objects but would be a pain for more complicated APIs where the SUT depended on the return values. Also, what if you want to test the calls to the dependency object in the same was you can with mock objects? Is there a way of mocking objects which are instantiated by the SUT rather than being passed in?

对于小对象来说这似乎是可行的,但对于更复杂的API而言,这将是一个痛苦,其中SUT依赖于返回值。另外,如果你想测试对依赖项对象的调用,那么你可以使用模拟对象吗?有没有一种方法可以模拟由SUT实例化的对象而不是传入?

I've read the man page on mocks but it doesn't seem to cover this situation where the dependency is composed rather than aggregated. How do you do it?

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

4 个解决方案

#1


As you seem to be aware already, Concrete Class Dependencies makes testing hard (or outright impossible). You need to decouple that dependency. A simple change, that doesn't break the existing API, is to default to the current behaviour, but provide a hook to override it. There are a number of ways that this could be implemented.

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

Some languages have tools that can inject mock classes into code, but I don't know of anything like this for PHP. In most cases, you would probably be better off refactoring your code anyway.

有些语言的工具可以将模拟类注入到代码中,但我不知道PHP的这类内容。在大多数情况下,无论如何,最好还是重构代码。

#2


Following troelskn advise here's a basic example of what you should do.

跟随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
    }
}

More information about mock objects can be found in the manual.

有关模拟对象的更多信息,请参见手册。

#3


Looks like I misunderstood the question, let me try again:

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

You should use the singleton pattern or a factory for the logger, if it's not too late already:

你应该使用单件模式或工厂作为记录器,如果已经不太晚了:

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

If you can't change the code, you could use a catch-all class that is overloading __call()

如果您无法更改代码,则可以使用重载__call()的catch-all类

class GenericStub {
    public function __call($functionName, $arguments) {}
}

#4


There is actually a reasonably new extension for PHP class overloading released by the same guys that build PHPUnit. It lets you override the new operator in cases where you can't refactor the code, unfortunately it isn't that simple to install on Windows.

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

The URL is http://github.com/johannes/php-test-helpers/blob/master/

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

#1


As you seem to be aware already, Concrete Class Dependencies makes testing hard (or outright impossible). You need to decouple that dependency. A simple change, that doesn't break the existing API, is to default to the current behaviour, but provide a hook to override it. There are a number of ways that this could be implemented.

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

Some languages have tools that can inject mock classes into code, but I don't know of anything like this for PHP. In most cases, you would probably be better off refactoring your code anyway.

有些语言的工具可以将模拟类注入到代码中,但我不知道PHP的这类内容。在大多数情况下,无论如何,最好还是重构代码。

#2


Following troelskn advise here's a basic example of what you should do.

跟随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
    }
}

More information about mock objects can be found in the manual.

有关模拟对象的更多信息,请参见手册。

#3


Looks like I misunderstood the question, let me try again:

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

You should use the singleton pattern or a factory for the logger, if it's not too late already:

你应该使用单件模式或工厂作为记录器,如果已经不太晚了:

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

If you can't change the code, you could use a catch-all class that is overloading __call()

如果您无法更改代码,则可以使用重载__call()的catch-all类

class GenericStub {
    public function __call($functionName, $arguments) {}
}

#4


There is actually a reasonably new extension for PHP class overloading released by the same guys that build PHPUnit. It lets you override the new operator in cases where you can't refactor the code, unfortunately it isn't that simple to install on Windows.

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

The URL is http://github.com/johannes/php-test-helpers/blob/master/

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