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

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

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

Я что-то упускаю?

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

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

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

  • TotT: использование инъекции зависимостей, чтобы избежать синтотов
  • Синглтоны – патологические лжецы
  • Недостаток: хрупкое глобальное государство и синглтоны

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

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

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

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