Как тяжело тестировать шаблон реестра или синглтон в PHP? - PullRequest
14 голосов
/ 12 марта 2011

Почему тестирование синглетонов или шаблона реестра сложно на языке, таком как PHP, который управляется запросом?

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

Я что-то упустил?

Ответы [ 3 ]

35 голосов
/ 12 марта 2011

Несмотря на то, что "вы можете писать и запускать тесты помимо фактического выполнения программы, так что вы можете свободно влиять на глобальное состояние программы и запускать некоторые разборки и инициализацию для каждой тестовой функции, чтобы получить ее в одно и то же состояние для каждого теста. ", это утомительно. Вы хотите протестировать TestSubject изолированно и не тратить время на воссоздание рабочей среды.

Пример

class MyTestSubject
{
    protected $registry;

    public function __construct()
    {
        $this->registry = Registry::getInstance();
    }
    public function foo($id)
    {
        return $this->doSomethingWithResults(
            $registry->get('MyActiveRecord')->findById($id)
        );
    }
}

Чтобы это заработало, вам нужен бетон Registry. Это жестко, и это синглтон. Последнее означает предотвращение любых побочных эффектов от предыдущего теста. Он должен быть сброшен для каждого теста, который вы будете запускать на MyTestSubject. Вы можете добавить метод Registry::reset() и вызвать его в setup(), но добавление метода только для возможности тестирования кажется уродливым. Давайте предположим, что вам все равно нужен этот метод, так что вы получите

public function setup()
{
    Registry::reset();
    $this->testSubject = new MyTestSubject;
}

Теперь у вас все еще нет объекта 'MyActiveRecord', который он должен вернуть в foo. Поскольку вам нравится Registry, ваш MyActiveRecord на самом деле выглядит следующим образом

class MyActiveRecord
{
    protected $db;

    public function __construct()
    {
        $registry = Registry::getInstance();
        $this->db = $registry->get('db');
    }
    public function findById($id) { … }
}

В конструкторе MyActiveRecord есть еще один вызов Registry. Ваш тест должен убедиться, что он содержит что-то, иначе тест не пройдёт. Конечно, наш класс базы данных также является синглтоном и должен быть сброшен между тестами. Doh!

public function setup()
{
    Registry::reset();
    Db::reset();
    Registry::set('db', Db::getInstance('host', 'user', 'pass', 'db'));
    Registry::set('MyActiveRecord', new MyActiveRecord);
    $this->testSubject = new MyTestSubject;
}

Итак, после окончательной настройки вы можете выполнить тест

public function testFooDoesSomethingToQueryResults()
{
    $this->assertSame('expectedResult', $this->testSubject->findById(1));
}

и осознайте, что у вас есть еще одна зависимость: ваша физическая тестовая база данных еще не настроена. Пока вы настраивали тестовую базу данных и заполняли ее данными, ваш начальник пришел и сказал, что вы собираетесь SOA сейчас, и все эти вызовы базы данных должны быть заменены на Web-сервис звонки.

Для этого есть новый класс MyWebService, и вы должны заставить MyActiveRecord использовать его вместо этого. Отлично, именно то, что вам нужно. Теперь вам нужно изменить все тесты, которые используют базу данных. Черт возьми, ты думаешь. Все это дерьмо, чтобы убедиться, что doSomethingWithResults работает как положено? MyTestSubject на самом деле все равно, откуда берутся данные.

Представляем макеты

Хорошая новость заключается в том, что вы действительно можете заменить все зависимости с помощью заглушек или макетировать их. Двойной тест притворится реальным.

$mock = $this->getMock('MyWebservice');
$mock->expects($this->once())
     ->method('findById')
     ->with($this->equalTo(1))
     ->will($this->returnValue('Expected Unprocessed Data'));

Это создаст дубликат для веб-службы, которая ожидает, что будет вызываться один раз во время теста с первым аргументом метода findById равен 1. Он будет возвращать предопределенные данные.

После того, как вы поместите это в метод в TestCase, ваш setup станет

public function setup()
{
    Registry::reset();
    Registry::set('MyWebservice', $this->getWebserviceMock());
    $this->testSubject = new MyTestSubject;
}

Отлично. Вам больше не нужно беспокоиться о создании реальной среды сейчас. Ну, кроме реестра. Как насчет насмешек над этим тоже. Но как это сделать. Он жестко запрограммирован, поэтому его невозможно заменить во время выполнения теста. Дерьмо!

Но подождите секунду, разве мы не сказали, что MyTestClass не волнует, откуда поступают данные? Да, просто важно, что он может вызывать метод findById. Надеюсь, вы сейчас думаете: почему вообще существует Реестр? И ты прав. Давайте изменим все на

class MyTestSubject
{
    protected $finder;

    public function __construct(Finder $finder)
    {
        $this->finder = $finder;
    }
    public function foo($id)
    {
        return $this->doSomethingWithResults(
            $this->finder->findById($id)
        );
    }
}

Byebye Registry. Теперь мы вводим зависимость MyWebSe… err… Finder ?! Да уж. Мы просто заботимся о методе findById, поэтому мы сейчас используем интерфейс

interface Finder
{
    public function findById($id);
}

Не забудьте изменить макет соответственно

$mock = $this->getMock('Finder');
$mock->expects($this->once())
     ->method('findById')
     ->with($this->equalTo(1))
     ->will($this->returnValue('Expected Unprocessed Data'));

и setup () становится

public function setup()
{
    $this->testSubject = new MyTestSubject($this->getFinderMock());
}

Вуаля! Красиво и легко. Теперь мы можем сосредоточиться на тестировании MyTestClass.

Пока вы этим занимались, ваш босс снова позвонил и сказал, что хочет, чтобы вы переключились обратно на базу данных, потому что SOA - это просто модное слово, используемое консультантами с завышенными ценами, чтобы заставить вас чувствовать себя предприимчивым. На этот раз вы не беспокоитесь, потому что вам не нужно снова менять тесты. Они больше не зависят от окружающей среды.

Конечно, вы все равно должны убедиться, что и MyWebservice, и MyActiveRecord реализуют интерфейс Finder для вашего реального кода, но, поскольку мы предполагали, что у них уже есть эти методы, это всего лишь вопрос установки implements Finder на класс.

И это все.Надеюсь, что это помогло.

Дополнительные ресурсы:

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

Это должно представлять наибольший интерес, поскольку оно написано автором PHPUnit и объясняет трудности с реальными примерами в PHPUnit.

Также представляют интерес:

5 голосов
/ 12 марта 2011

Синглеты (во всех языках ООП, не только в PHP) затрудняют особый вид отладки, называемый модульным тестированием по той же причине, что и глобальные переменные. Они вводят глобальное состояние в программу, что означает, что вы не можете тестировать любые модули вашего программного обеспечения, которые зависят от изолированного синглтона. Модульное тестирование должно включать только тестируемый код (и его суперклассы).

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

2 голосов
/ 15 июля 2014

По завершении теста PHP вы можете сбросить экземпляр синглтона следующим образом:

protected function tearDown()
{
    $reflection = new ReflectionClass('MySingleton');
    $property = $reflection->getProperty("_instance");
    $property->setAccessible(true);
    $property->setValue(null);
}
...