Как протестировать шаблон реестра или singleton hard в PHP?

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

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

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

Ответ 1

Хотя верно, что "вы можете писать и запускать тесты в стороне от фактического выполнения программы, чтобы вы могли влиять на глобальное состояние программы и запускать некоторые срывы и инициализацию на каждую тестовую функцию, чтобы получить ее к тому же для каждого теста". Это утомительно. Вы хотите протестировать 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. Он жестко закодирован, и это Синглтон. Последнее означает предотвращение любых побочных эффектов от предыдущего теста. Он должен быть reset для каждого теста, который вы будете запускать в 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 есть еще один вызов реестра. Вы проверяете, что он содержит что-то, иначе тест не удастся. Конечно, наш класс базы данных также является Singleton и должен быть reset между тестами. 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 service.

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

Знакомство с mocks

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

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

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

После того, как вы поместите это в свой метод в свою тестовую систему, ваш 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());
}

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

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

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

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

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

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

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

Также интересны:

Ответ 2

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

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

Ответ 3

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

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