Если вы являетесь частью команды разработчиков, чаще всего ваш код также будет зависеть от кода, написанного вашими товарищами по команде. Но что, если их код в данный момент недоступен — скажем, ваш товарищ по команде еще не закончил его писать? Или, что если для кода, который вам нужен, требуются другие внешние зависимости, которые сложно настроить? А что если вы не можете протестировать код из-за других факторов, не зависящих от вас? Будете ли вы просто бездельничать, ничего не делать и ждать, пока ваша команда не закончит или когда все будет на месте? Конечно, нет!
В этой статье я покажу, как написать код, который решает эту проблему с зависимостями. Некоторые основы модульного тестирования идеальны, и здесь уже есть отличная вводная статья на SitePoint о модульном тестировании, написанная Мишель Савер. И хотя это не требуется для этой статьи, ознакомьтесь с другими моими статьями по автоматическому тестированию баз данных.
Дело о ложных объектах
Как вы, возможно, уже догадались, фиктивные объекты — это выход из сложных ситуаций, о которых я упоминал во введении. Но что такое фиктивные объекты? Поддельные объекты — это объекты, которые представляют собой реальную реализацию реального объекта.
Теперь, почему вы хотите, чтобы объект-заменитель вместо реального?
Поддельные объекты используются в модульном тестировании для имитации поведения реальных объектов в тестовых примерах. Используя их, функциональность объекта, который вы реализуете, будет легче протестировать. Вот несколько ситуаций, в которых использование фиктивного объекта выгодно:
- Реальная реализация одной или нескольких зависимостей объекта еще не реализована. Допустим, вам поручено выполнить некоторую обработку некоторых данных из базы данных. Возможно, вы бы вызвали метод какой-либо формы объекта доступа к данным или хранилища данных, но что, если база данных еще не настроена? Что если данных не было (ситуация, с которой я сталкивался слишком много раз) или код, который запрашивает базу данных, еще не написан? Имитируемый объект доступа к данным имитирует реальный объект доступа к данным, возвращая некоторые предварительно определенные значения. Это освобождает вас от необходимости настраивать базу данных, искать данные или писать код, который запрашивает базу данных.
- Реальная реализация зависимостей объекта зависит от факторов, которые трудно смоделировать. Предположим, вы захотите составить таблицу лайков и комментариев поста в Facebook по дням. Где вы получите свои данные? Ваш аккаунт разработчика Facebook новый. Черт возьми, у тебя до сих пор нет друзей! Как вы будете имитировать лайки и комментарии? Ложные объекты предлагают лучший подход, чем мешать вашим коллегам-разработчикам понравиться или прокомментировать некоторые посты для вас. И как вы будете имитировать все эти действия в течение нескольких дней, если вы хотите показывать данные по дням? Как насчет месяца? Что вы делаете, если происходит немыслимое, а Facebook в данный момент не работает? Поддельный объект может притвориться библиотекой Facebook и вернуть нужные вам данные. Вам не нужно проходить через трудности, которые я только что упомянул, чтобы начать работать над своей задачей.
Макет объектов в действии
Теперь, когда мы знаем, что такое фиктивные объекты, давайте рассмотрим несколько примеров в действии. Мы реализуем нашу простую функциональность, о которой говорилось выше, например, добавление таблиц лайков и комментариев в пост на Facebook.
Мы начнем со следующего модульного теста, чтобы определить наши ожидания относительно того, как будет вызываться наш объект и как будет выглядеть возвращаемое значение:
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { private $statusService; private $fbID = 1; public function setUp() { $this->statusService = new StatusService(); } public function testGetAnalytics() { $analytics = $this->statusService->getAnaltyics(1, strtotime("2012-01-01"), strtotime("2012-01-02")); $this->assertEquals(array( "2012-01-01" => array( "comments" => 5, "likes" => 3, ), "2012-01-02" => array( "comments" => 5, "likes" => 3, ), "2012-01-03" => array( "comments" => 5, "likes" => 3, ), "2012-01-04" => array( "comments" => 5, "likes" => 3, ), "2012-01-05" => array( "comments" => 5, "likes" => 3, ) ), $analytics); } }
Если вы запустите этот тест, вы получите сбой. Это ожидается, так как мы еще ничего не реализовали!
Теперь давайте напишем нашу реализацию сервиса. Конечно, первым шагом является получение данных из Facebook, поэтому давайте сделаем это сначала:
<?php class StatuService { private $facebook; public function getAnalytics($id, $from, $to) { $post = $this->facebook->get($id); } }
Этот тест также не пройден, потому что объект Facebook является нулевым. Вы можете подключить реальную реализацию, создав реальный экземпляр с идентификатором приложения Facebook и т. Д., Но зачем? Мы уже все знаем, что такое агония, чтобы сделать это, когда она позволяет вам отклониться от поставленной задачи. Мы можем сэкономить, введя вместо этого ложный!
Способ, которым это делается с использованием фиктивных объектов, по крайней мере, в нашем случае, заключается в создании класса, который имеет метод get()
и возвращает ложные значения. Это должно заставить нашего клиента думать, что он вызывает реальную реализацию объекта, хотя на самом деле это просто высмеивали.
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { // test here } class MockFacebookLibrary { public function get($id) { return array( // mock return from Facebook here ); } }
Теперь, когда у нас есть фиктивный класс, давайте StatusService
экземпляр и StatusService
его в StatusService
чтобы его можно было использовать. Но сначала обновите StatusService
установщиком для библиотеки Facebook:
<?php class StatusService { // other lines of code public function setFacebook($facebook) { $this->facebook = facebook; } }
Теперь добавьте ложную библиотеку Facebook:
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { public function testGetAnalytics { $analytics = $this->statusService->getAnaltyics(1, strtotime("2012-01-01"), strtotime("2012-01-02")); $analytics->setFacebook(new MockFacebook()); // code truncated } }
Тест по-прежнему не проходит, но по крайней мере мы больше не получаем ошибку, связанную с вызовом метода для необъекта. Что еще более важно, вы только что обратились к необходимости выполнить эту зависимость. Теперь вы можете начать программировать бизнес-логику, которую вам поручено сделать, и пройти тест.
PHPUnit 3.6.7 от Себастьяна Бергманна. F Время: 0 секунд, память: 3.00Mb Был 1 сбой: 1) StatusServiceTest :: testGetAnalytics null не соответствует ожидаемому типу «массив». /home/jeune/mock-object-tutorial/ManualMockStatusServiceTest.php:43 ОТКАЗЫ! Тесты: 1, Утверждения: 1, Неудачи: 1.
Сделаем шаг вперед: использование Mocking Framework
Хотя, конечно, вы можете жить с использованием созданных вручную макетов, когда только начинаете, позже, как я сам понял, у вас может возникнуть необходимость использовать реальный макет макета, поскольку ваши потребности становятся все более сложными. В этой статье я покажу, как использовать фреймворк, который поставляется с PHPUnit.
По моему опыту, вот некоторые преимущества использования фиктивной среды по сравнению с использованием написанных вручную макетов:
- Вы можете позволить себе быть ленивым. Я нашел это особенно верно, если вы имеете дело с абстрактными классами с большим количеством абстрактных методов. Вы можете позволить себе издеваться только над определенными методами абстрактного класса или интерфейса. Если бы вы делали это вручную, то вам пришлось бы реализовывать их все вручную. Это экономит ваше время при наборе текста и поставляется в комплекте с подсказками типа; вы только настраиваете то, что вам нужно, и вам не нужно поддерживать новый класс для каждого теста.
- Вы можете написать более чистый код. Читаемость является ключом здесь. Мок-фреймы на основе фреймворка облегчают понимание ваших тестов, так как ваши макеты написаны в тесте. Вам не нужно прокручивать вниз или переключаться между файлами для просмотра рукописных макетов, написанных в другом месте. Что делать, если вам нужно вызывать ваш фиктивный объект более одного раза с разными результатами? При использовании макетов на основе фреймворка код котельной плиты if-else, необходимый для этого, уже хорошо инкапсулирован. Следовательно, это легче для глаз.
Использование PHPUnit Mocking Framework
Обращая наше внимание на использование Mocking Framework в PHPUnit, шаги на самом деле довольно интуитивны, и, как только вы освоите их, это станет второй натурой. В этом разделе мы будем использовать Mocking Framework в PHPUnit для создания фиктивного объекта в нашем примере ситуации.
Однако перед тем, как мы это сделаем, закомментируйте или удалите строку, которая использует наш фиктивный объект ручной работы в нашем тесте. Сначала мы должны потерпеть неудачу, поэтому нам есть что пройти. Позже мы добавим нашу новую фиктивную реализацию.
<?php class StatusServiceTest extends PHPUnit_Framework_TestCase { public function testGetAnalytics { $analytics = $this->statusService->getAnaltyics(1, strtotime("2012-01-01"), strtotime("2012-01-02")); //$analytics->setFacebook(new MockFacebook()); // code truncated } }
Убедитесь, что тест действительно не проходит при запуске PHPUnit.
Теперь подумайте о том, как мы смоделировали объект и метод вручную, который мы хотим вызвать. Что мы сделали?
- Первый шаг — определить, какие объекты нужно издеваться. В приведенном выше примере с аналитикой мы издевались над библиотекой Facebook. Мы делаем то же самое, что и первый шаг.
- Теперь, когда мы определили, какой класс имитировать, мы должны знать, какие методы в классе мы хотим имитировать, задав параметры и возвращая значения, если они есть. Базовый шаблон, который я использую, в большинстве случаев выглядит примерно так:
- Укажите, сколько раз будет вызываться метод (обязательно).
- Укажите имя метода (обязательно).
- Укажите параметры, которые ожидает метод (необязательно).
- Укажите возвращаемое значение (необязательно)
Давайте применим только что упомянутые шаги к нашему примеру теста.
<?php //code truncated public function testGetAnalytics() { $arr = array(// facebook return); $facebook = $this->getMock('facebook') // object to mock ->expects($this->once()) // number of times to be called ->method('get') // method name ->with(1) // parameters that are expected ->will($this->returnValue($arr)); // return value // code truncated }
После того, как мы снова создадим наш фиктивный объект facebook, добавьте его обратно в наш сервис:
<?php //code truncated public function testGetAnalytics() { $arr = array(// facebook return); $facebook = $this->getMock('facebook') // object to mock ->expects($this->once()) // number of times to be called ->method('get') // method name ->with(1) // parameters that are expected ->will($this->returnValue($arr)); // return value $this->statusService->setFacebook($facebook); // code truncated }
Теперь вы должны пройти повторный тест снова. Поздравляем! Вы начали работать с использованием фиктивных объектов для тестирования! Надеюсь, вы сможете программировать более эффективно и, более всего, освободитесь от зависимостей show stopper, с которыми вы столкнетесь в будущем.
Изображение через Fotolia