PropelORM, Symfony 2 и модульное тестирование - PullRequest
3 голосов
/ 12 марта 2012

Я привык писать так:

$results = SomeModelQuery::create()->filterByFoo('bar')->find();

Однако это не подходит для модульного тестирования, потому что я не могу ввести фиктивный объект, то есть не могу повлиять на то, какие данныевозвращаетсяЯ хотел бы использовать данные о приборах, но не могу.

И при этом не кажется хорошим вводить объект:

class Foo
{
    public __construct($someModelQuery)
    {
        $this->someModelQuery = $someMOdelQuery;
    }

    public function doSthing()
    {
         $results = $this->someModelQuery->filterByFoo('bar')->find();
    }
}

Я чувствую себя ужасно.У меня есть десятки объектов запроса, чтобы высмеивать и бросать.Настройка через конструктор уродлива и болезненна.Установка с использованием метода неправильна, потому что это может быть забыто при вызове.И всегда больно для каждой отдельной библиотеки и действия создавать эти объекты запросов вручную.

Как бы я элегантно сделал DI с классами запросов PropelORM?Я не хочу вызывать такой метод, как:

$oneQuery = OneQuery::create();
$anotherQuery = AnotherQuery::create();
// ... 10 more ...
$foo = new Foo($oneQuery, $anotherQuery, ...);
$foo->callSomeFunctionThatNeedsThose();

1 Ответ

3 голосов
/ 15 марта 2012

На мой взгляд (и Мартина Фоловера ) есть шаг между статическим вызовом всего и использованием инъекции зависимости, и это может быть то, что вы ищете.

Там, где я не могу сделать полный DI (например, Zend Framework MVC), я буду использовать Service Locator. Сервисный уровень будет местом, откуда все ваши классы будут получать зависимости. Думайте об этом как об однослойной глубокой абстракции для ваших зависимостей классов. Использование сервисного локатора дает много преимуществ, но в этом случае я остановлюсь на тестируемости.

Давайте разберемся в некотором коде, вот класс модели запроса

class SomeModelQuery
{
    public function __call($method, $params)
    {
        if ($method == 'find') {
            return 'Real Data';
        }
        return $this;
    }
}

Все, что он делает, это возвращает себя, если только не вызван метод 'find'. Затем вернет жестко закодированную строку «Реальные данные».

Теперь наш сервисный локатор:

class ServiceLocator
{
    protected static $instance;

    protected $someModelQuery;

    public static function resetInstance()
    {
        static::$instance = null;
    }

    public static function instance()
    {
        if (self::$instance === null) {
            static::$instance = new static();
        }
        return static::$instance;
    }

    public function getSomeModelQuery()
    {
        if ($this->someModelQuery === null) {
            $this->someModelQuery = new SomeModelQuery();
        }
        return $this->someModelQuery;
    }

    public function setSomeModelQuery($someModelQuery)
    {
        $this->someModelQuery = $someModelQuery;
    }
}

Это делает две вещи. Предоставляет экземпляр метода глобальной области видимости, чтобы вы всегда могли его найти. Наряду с возможностью сброса. Затем предоставление методов get и set для объекта запроса модели. С отложенной загрузкой, если она еще не установлена.

Теперь код, который выполняет реальную работу:

class Foo
{
    public function doSomething()
    {
        return ServiceLocator::instance()
            ->getSomeModelQuery()->filterByFoo('bar')->find();
    }
}

Foo вызывает локатор службы, затем получает от него экземпляр объекта запроса и выполняет необходимый вызов для этого объекта запроса.

Так что теперь нам нужно написать несколько юнит-тестов для всего этого. Вот оно:

class FooTest extends PHPUnit_Framework_TestCase
{
    protected function setUp()
    {
        ServiceLocator::resetInstance();
    }

    public function testNoMocking()
    {
        $foo = new Foo();
        $this->assertEquals('Real Data', $foo->doSomething());
    }

    public function testWithMock()
    {
        // Create our mock with a random value
        $rand = mt_rand();
        $mock = $this->getMock('SomeModelQuery');
        $mock->expects($this->any())
            ->method('__call')
            ->will($this->onConsecutiveCalls($mock, $rand));
        // Place the mock in the service locator
        ServiceLocator::instance()->setSomeModelQuery($mock);

        // Do we get our random value back?
        $foo = new Foo();
        $this->assertEquals($rand, $foo->doSomething());
    }
}

Я привел пример, где вызывается реальный код запроса и где код запроса имитируется.

Так что это дает вам возможность внедрять макеты без необходимости вставлять каждую зависимость в классы, которые вы хотите тестировать модулем.

Существует много способов написания приведенного выше кода. Используйте это как доказательство концепции и адаптируйте ее под свои нужды.

...