Тестирование объектов с зависимостями в PHPUnit

Для объектов, которые составляют другой объект как часть их реализации, как лучше всего написать модульный тест, чтобы тестировался только основной объект? Тривиальный пример:

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 
{
    // ...
}

Это кажется выполнимым для небольших объектов, но будет проблемой для более сложных API, где ТРИ зависит от возвращаемых значений. Кроме того, что, если вы хотите протестировать вызовы объекта зависимости так же, как и с фиктивными объектами? Есть ли способ издеваться над объектами, которые создаются SUT, а не передаются?

Я прочитал справочную страницу по макетам, но, похоже, она не охватывает эту ситуацию, когда зависимость составляется, а не агрегируется. Как ты сделал это?


person DavidWinterbottom    schedule 13.02.2009    source источник
comment
Вы смотрели на PHPUnit, который представил тестовые зависимости и, следовательно, повторное использование фикстуры, начиная с 3.4? sebastian-bergmann.de/archives/ Ура Маркус   -  person    schedule 30.11.2009


Ответы (4)


Как вы, кажется, уже знаете, конкретные зависимости классов делают тестирование трудным (или совершенно невозможным). Вам нужно отделить эту зависимость. Простое изменение, которое не нарушает существующий API, заключается в том, чтобы по умолчанию использовать текущее поведение, но предоставить хук для его переопределения. Есть несколько способов, как это можно реализовать.

В некоторых языках есть инструменты, которые могут внедрять фиктивные классы в код, но я не знаю ничего подобного для PHP. В большинстве случаев вам, вероятно, будет лучше провести рефакторинг кода.

person troelskn    schedule 13.02.2009
comment
Похоже, вы говорите, что любая форма агрегации плоха, поскольку это конкретная зависимость, которая является жесткой связью и, следовательно, ее трудно протестировать. Это правильно? Я думаю, что согласен, но не приведет ли это к тяжелому скрипту начальной загрузки, где все объекты зависимостей вводятся туда, где они нужны? - person DavidWinterbottom; 13.02.2009
comment
@troelskn, просто из любопытства, не могли бы вы привести пример инструментов для тех языков, о которых вы говорите. - person Ionuț G. Stan; 13.02.2009
comment
@david Да в первой части и вроде да во второй части, за исключением того, что тяжесть субъективна. Контейнеры DI — это способ немного облегчить боль. - person troelskn; 13.02.2009
comment
@ionut Я думал о code.google.com/p/powermock, который использует пользовательский загрузчик классов. - person troelskn; 13.02.2009

Следуя совету 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
    }
}

Дополнительную информацию о фиктивных объектах можно найти в руководстве.

person Ionuț G. Stan    schedule 13.02.2009

Похоже, я неправильно понял вопрос, позвольте мне попробовать еще раз:

Вы должны использовать шаблон singleton или factory для регистратора, если еще не поздно:

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

Если вы не можете изменить код, вы можете использовать универсальный класс, который перегружает __call()

class GenericStub {
    public function __call($functionName, $arguments) {}
}
person soulmerge    schedule 13.02.2009

На самом деле существует достаточно новое расширение для перегрузки классов PHP, выпущенное теми же ребятами, которые создают PHPUnit. Он позволяет вам переопределить новый оператор в тех случаях, когда вы не можете реорганизовать код, к сожалению, его не так просто установить в Windows.

URL-адрес: http://github.com/johannes/php-test-helpers/blob/master/

person Community    schedule 05.03.2010