В январе Мигель Ромеро написал отличную статью, показывающую, как начать работу с Guzzle. Если вы пропустили статью или не знакомы с Guzzle:
Guzzle — это PHP HTTP-клиент и фреймворк для создания клиентов веб-сервисов RESTful.
В статье Мигеля он охватил ряд функций библиотеки, включая базовую конфигурацию для запросов GET и POST, плагины, такие как ведение журнала с использованием monolog, и закончил взаимодействие со службой OAuth, в частности с API GitHub.
В этом уроке я хочу показать вам, как использовать Guzzle с другой точки зрения, в частности, как проводить модульное тестирование с ним. Для этого мы рассмотрим три конкретных подхода:
- Ручная обработка пользовательских ответов
- Использование ServiceClient с фиктивными файлами ответов
- Поставить сервер в очередь с ложными ответами
Начало настройки
Примечание. Исходный код этой статьи доступен на 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, но я надеюсь, что пробудил ваше любопытство, продемонстрировав вам множество способов убедиться, что ваш клиентский код полностью покрыт тестированием.
Какой у тебя опыт? Поделитесь своими мыслями в комментариях.