Статьи

Модульное тестирование с GuzzlePHP

В январе Мигель Ромеро написал отличную статью, показывающую, как начать работу с Guzzle. Если вы пропустили статью или не знакомы с Guzzle:

Guzzle — это PHP HTTP-клиент и фреймворк для создания клиентов веб-сервисов RESTful.

В статье Мигеля он охватил ряд функций библиотеки, включая базовую конфигурацию для запросов GET и POST, плагины, такие как ведение журнала с использованием monolog, и закончил взаимодействие со службой OAuth, в частности с API GitHub.

В этом уроке я хочу показать вам, как использовать Guzzle с другой точки зрения, в частности, как проводить модульное тестирование с ним. Для этого мы рассмотрим три конкретных подхода:

  1. Ручная обработка пользовательских ответов
  2. Использование ServiceClient с фиктивными файлами ответов
  3. Поставить сервер в очередь с ложными ответами

Начало настройки

Примечание. Исходный код этой статьи доступен на Github .

Как и все, нам нужно идти, прежде чем мы сможем бежать. В этом случае нам нужно настроить нашу тестовую среду и тестовый класс. Чтобы получить все на месте, мы будем использовать Composer. Если вы не знакомы с Composer, пожалуйста, прочитайте статью Александра здесь на SitePoint, прежде чем продолжить.

Наш файл composer.json будет выглядеть следующим образом:

{ "require": { "php": ">=5.3.3", } "require-dev": { "phpunit/phpunit": "4.0.*", "guzzle/guzzle": "~3.7" } } 

Я предусмотрел минимальную версию PHP 5.3.3. Чтобы быть справедливым, это, вероятно, должно быть выше, но это хорошее начало. Наши другие требования — это PHPUnit и Guzzle. После добавления их в composer.json, в вашем проекте запустите composer install и после небольшого ожидания зависимости будут готовы к работе.

Подготовка PHPUnit

Прежде чем мы сможем запустить наши модульные тесты, нам также нужно немного подготовиться. Во-первых, создайте в своем проекте каталог с именем tests . Там создайте два файла: bootstrap.php и phpunit.xml.dist .

bootstrap.php довольно прост:

 <?php error_reporting(E_ALL | E_STRICT); require dirname(__DIR__) . '/vendor/autoload.php'; 

Это включает автоматически сгенерированный файл autoload.php из каталога vendor, который создал Composer. Это гарантирует, что у нас есть доступ как к PHPUnit, так и к Guzzle. Далее давайте посмотрим на phpunit.xml.dist .

 <?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="./bootstrap.php" colors="true"> <testsuites> <testsuite name="importer-tests"> <directory suffix="Test.php">./</directory> </testsuite> </testsuites> </phpunit> во <?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="./bootstrap.php" colors="true"> <testsuites> <testsuite name="importer-tests"> <directory suffix="Test.php">./</directory> </testsuite> </testsuites> </phpunit> во <?xml version="1.0" encoding="UTF-8"?> <phpunit bootstrap="./bootstrap.php" colors="true"> <testsuites> <testsuite name="importer-tests"> <directory suffix="Test.php">./</directory> </testsuite> </testsuites> </phpunit> 

Это довольно элементарная конфигурация. Что это делает:

  • Скажите PHPUnit использовать bootstrap.php для начальной загрузки тестовой среды
  • Используйте цвета в тестовом выводе (удобно для определения аспектов импорта)
  • Установите один набор tests , называемый tests . Это ищет тесты во всех файлах, которые заканчиваются в Test.php , расположенном в любом месте в текущем каталоге

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

После этого давайте начнем строить наш тестовый класс. В каталоге тестов создайте новый файл с именем SitePointGuzzleTest.php . В него добавьте следующее:

 <?php use Guzzle\Tests\GuzzleTestCase, Guzzle\Plugin\Mock\MockPlugin, Guzzle\Http\Message\Response, Guzzle\Http\Client as HttpClient, Guzzle\Service\Client as ServiceClient, Guzzle\Http\EntityBody; class SitePointGuzzleTest extends GuzzleTestCase { protected $_client; } 

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

Ручная обработка пользовательских ответов

Из статьи Мигеля вы будете знакомы с инициализацией клиента для выполнения запроса, потенциальной передачей параметров, а затем проверкой ответа. Предположим, вы создали класс, который либо использует клиент Guzzle, либо использует его, либо использует объект ответа Guzzle.

Допустим, вы работаете с FreeAgent API и хотите протестировать код, который извлекает данные счета.

Вы хотите быть уверены, что ваш код реагирует так, как требуется, если что-то пойдет не так или API изменится. Давайте посмотрим, как смоделировать ответ, пройдя некоторый аннотированный код.

 public function testAnotherRequest() { $mockResponse = new Response(200); $mockResponseBody = EntityBody::factory(fopen( './mock/bodies/body1.txt', 'r+') ); $mockResponse->setBody($mockResponseBody); // ... } 

Здесь мы сначала создаем новый объект Response. Затем мы используем фабричный метод Guzzle\Http\EntityBody чтобы установить тело ответа с содержимым файла ./mock/bodies/body1.txt .

Это облегчает отделение конфигурации от кода и упрощает сопровождение теста. body1.txt доступен в исходном коде для этой статьи.

 $mockResponse->setHeaders(array( "Host" => "httpbin.org", "User-Agent" => "curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3", "Accept" => "application/json", "Content-Type" => "application/json" )); 

Затем мы передаем ассоциативный массив в setHeaders, который установит четыре пользовательских заголовка в нашем ответе.

 $plugin = new MockPlugin(); $plugin->addResponse($mockResponse); $client = new HttpClient(); $client->addSubscriber($plugin); 

Здесь мы создаем новый объект MockPlugin, передавая ему объект ответа. После того, как мы создаем наш новый HttpClient , мы добавляем MockPlugin в качестве подписчика, который, в свою очередь, гарантирует, что запросы, сделанные с ним, будут использовать ложный ответ, который мы только что создали.

 $request = $client->get( 'https://api.freeagent.com/v2/invoices' ); $response = $request->send(); 

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

 $this->assertEquals(200, $response->getStatusCode()); $this->assertTrue(in_array( 'Host', array_keys($response->getHeaders()->toArray()) )); $this->assertTrue($response->hasHeader("User-Agent")); $this->assertCount(4, $response->getHeaders()); $this->assertSame( $mockResponseBody->getSize(), $response->getBody()->getSize() ); $this->assertSame( 1, count(json_decode($response->getBody(true))->invoices )); 

В утверждениях вы можете видеть, что я проверил код ответа, если Host и User-Agent были в заголовках ответа, количество отправленных заголовков, что размер тела соответствовал размеру нашего поддельного тела ответа и что в полученном ответе был один счет.

Это лишь небольшая выборка видов тестов, которые можно запускать, но она показывает, насколько легко как смоделировать пользовательский ответ, так и проверить его при получении. Какие еще виды тестов вы бы провели?

Использование ServiceClient с фиктивными файлами ответов

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

На этот раз давайте переопределим setUp следующим образом:

 public function setUp() { $this->_client = new ServiceClient(); $this->setMockBasePath('./mock/responses'); $this->setMockResponse( $this->_client, array('response1') ); } 

Здесь мы $_client экземпляр переменной класса $_client как новый ServiceClient . Затем мы использовали setMockBasePath для ./mock/responses и вызывали setMockResponse , передавая наш клиентский объект и массив.

В массиве перечислены имена файлов в «./mock/responses». Содержимое этих файлов будет использоваться для настройки последовательности ответов, которые клиент будет получать при каждом последующем вызове для send .

В этом случае я добавил только один, но вы можете легко добавить столько, сколько захотите. В ./mock/responses/response1 вы можете видеть, что в нем перечислены версия HTTP, код состояния, заголовки и тело ответа. Еще раз, он сохраняет код и конфигурацию аккуратно разделенными.

Теперь давайте посмотрим на функцию, которая ее использует.

 public function testRequests() { $request = $this->_client->get( 'https://api.freeagent.com/v2/invoices' ); $request->getQuery()->set( 'view', 'recent_open_or_overdue' ); $response = $request->send(); $this->assertContainsOnly( $request, $this->getMockedRequests() ); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals( 'AmazonS3', $response->getServer() ); $this->assertEquals( 'application/xml', $response->getContentType() ); } 

Вы можете видеть, что все, что мне нужно было сделать, — это позвонить, чтобы get на клиентском объекте и отправить на возвращенный объект запроса, как мы обычно делаем. Я добавил в параметры запроса только для хорошей меры.

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

Стоит отметить, что это очередь FIFO ; Это означает, что первый добавленный ответ является первым, который будет отправлен. Не позволяй этой путанице тебя поднять.

Поставить сервер в очередь с ложными ответами

Наконец, давайте посмотрим на один из круче аспектов Guzzle. Если мы вызываем $this->getServer()->enqueue(array()); Гузл прозрачно запускает сервер node.js за кулисами. Затем мы можем использовать это для отправки запросов, как если бы это была наша реальная конечная точка сервера. Давайте посмотрим, как его использовать.

 public function testWithRemoteServer() { $mockProperties = array( array( 'header' => './mock/headers/header1.txt', 'body' => './mock/bodies/body1.txt', 'status' => 200 ) ); } 

Здесь я создаю массив для хранения заголовка, тела и информации о состоянии для ложного запроса, указав код состояния 200 и файлы, содержащие данные ответа заголовка и тела.

 $mockResponses = array(); foreach($mockProperties as $property) { $mockResponse = new Response($property['status']); $mockResponseBody = EntityBody::factory( fopen($property['body'], 'r+') ); $mockResponse->setBody($mockResponseBody); $headers = explode( "\n", file_get_contents($property['header'], true) ); foreach($headers as $header) { list($key, $value) = explode(': ', $header); $mockResponse->addHeader($key, $value); } $mockResponses[] = $mockResponse; } 

Затем я создал новый объект Response, установил код состояния и снова использовал фабричный метод класса EntityBody для установки тела. Заголовки были немного более громоздкими, поэтому я addHeader содержимое файла, вызвав addHeader для каждой полученной пары ключ / значение.

 $this->getServer()->enqueue($mockResponses); 

Каждый созданный enqueue объект ответа добавляется в массив, который затем передается в enqueue . Теперь у него есть набор ответов, готовых для отправки нашим клиентским запросам.

 $client = new HttpClient(); $client->setBaseUrl($this->getServer()->getUrl()); $request = $client->get(); $request->getQuery()->set( 'view', 'recent_open_or_overdue' ); $response = $request->send(); 

Здесь, как и прежде, мы инициализировали наш клиентский объект, вызывая get и send для возвращенного запроса. Стоит отметить, что для использования сервера Node.JS нам нужно передать $this->getServer()->getUrl() в $client->setBaseUrl() , иначе это не будет работать.

 $this->assertCount(5, $response->getHeaders()); $this->assertEmpty($response->getContentDisposition()); $this->assertSame('HTTP', $response->getProtocol()); 

После этого наш код работает так же, как и раньше, и я добавил утверждения для заголовков, заголовка расположения контента и протокола в возвращаемом объекте ответа.

Завершение

Так что ты думаешь? Несмотря на то, что это занимает немного работы, я уверен, что вы видите, как просто настроить различные подходы к ответу. Я уверен, немного поработав, вы также можете упростить подходы, которые я использовал.

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

Какой у тебя опыт? Поделитесь своими мыслями в комментариях.