При написании автоматических тестов к PHP приложению могут возникать различные сложности. Одной из таких сложностей может быть зависимость от внешнего сервиса, к которому (используя его API) происходит обращение по HTTP. Конечно делать реальные запросы к сторонним ресурсам в тестах – не выход. Поэтому в определенный момент придется прибегнуть к созданию различных заглушек. В этой статье попробуем собрать различные способы использования заглушек.
Содержание статьи
- Виды заглушек
- Создание заглушки для GuzzleHttp\Client
- Обращения к внешним ресурсам и тесты используя Laravel
Виды заглушек
Разберем кратко какие виды заглушек бывают.
- Пустышки (dummies) могут быть полезны, если работаете со старым кодом, где не используются подсказки типа (type hints), а вам в тесте нужна зависимость, методы которой вы не будете вызывать.
- Стабы пригодятся, если вам нужна пустышка в типизированном коде (если методы также не будут вызываться). Тоесть тестируемый код зависит от некоторого класса, методы которого вызываться не будут.
- Шпионы — если вам нужен стаб с вызовом методов, которые ничего делают (важно, чтобы они могли вызываться и только)
- Моки — если вам нужна зависимость, где будут вызываться определённые методы, которые должны что-то вернуть или выполнить согласно тесту
Правда в реальной жизни (при использовании фреймворков для тестирования) можно встретить как одно называют другим, добавляют дополнительную функциональность и размывают границы между этими видами заглушек. Но для глубокого понимания, а также случая, когда понадобится тестировать код без любимого фреймворка, полезно знать исходный вариант и понимать, что заглушка это не всегда Mockery::mock
. Заглушку можно сотворить и создав пустой класс или класс наследник от рабочей реализации. Главное, чтобы ваш контейнер зависимостей умел подменить рабочую реализацию на тестовую.
Создание заглушки для GuzzleHttp\Client
Вернемся к вопросу тестирования кода с HTTP запросами к внешним сайтам. Первое, что может помочь, в этом – проектирование тестируемого кода так, чтобы классы отвечающие за обращения к внешним API были четко выделены и позволяли заменить их на заглушки. Тогда все относительно просто. Мы подменяем наши классы на другие, которые возвращают такой результат как и классы, обращающиеся к сторонним URL.
Но не всегда тесты приходят в проект с его началом. Как и хорошая архитектура не всегда приходит сразу. Если классы в проекте не так грамотно спроектированы, то есть надежда, что в нем хотя бы используется для HTTP запросов популярная и продуманная библиотека.
Допустим, используется Guzzle. В коде делается HTTP запрос через метод post()
класса GuzzleHttp\Client
. Тогда код для создания заглушки в тесте с помощью Mockery::mock
может выглядеть примерно так:
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Utils;
//...
app()->bind(Client::class, function() {
$client = Mockery::mock(Client::class);
$stream = Utils::streamFor('SUCCESS');
$response = new Response(200, [], $stream);
$client->shouldReceive('post')
->times(1)
->andReturn($response);
return $client;
});
Здесь правда используется контейнер зависимостей Laravel, который с помощью app()->bind()
заменяет реальный класс GuzzleHttp\Client
на заглушку, которая при вызове метода post()
вернет HTTP код 200 и строку ‘SUCCESS’ как тело ответа. Но в общем-то контейнер зависимостей можно применить любой другой, если проект не на Laravel.
Отмечу, что, если надо чтобы метод у заглушки возвращал разные ответы для разных URL, то при создании заглушки необходимо будет дописать немного кода, чтобы это учесть.
Сам тестируемый код для примера выше мог бы выглядеть примерно так:
use GuzzleHttp\Client;
//...
$data = [
// ....
];
$client = app()->make(Client::class, ['http_errors' => false]);
// Обращение к внешнему ресурсу через HTTP POST
$response = $client->post($url, [ 'verify' => false, RequestOptions::JSON => $data ]);
$httpCode = $response->getStatusCode();
$responseText = $response->getBody();
if ($httpCode !== 200 or $responseText !== 'SUCCESS') {
$this->log()
->error("Результат - ERROR. URL: {$url}: [$httpCode] $responseText");
}
Здесь важно, что класс GuzzleHttp\Client
также подгружается из контейнера, а не создается через new
. Это позволяет при запуске тестов приложению подменять реальную зависимость на заглушку.
Обращения к внешним ресурсам и тесты используя Laravel
Если же проект на Laravel, то можно использовать и его удобную обертку к Guzzle HTTP client.
Фасад Http позволяет творить маленькие чудеса:
use Illuminate\Support\Facades\Http;
// Простой запрос
$response = Http::get('http://example.com');
// Сразу работаем с JSON
$userName = Http::get('http://example.com/users/1')['name'];
// Отправляем данные (по-умолчанию в JSON)
$response = Http::post('http://example.com/users', [
'name' => 'Steve',
'role' => 'Network Administrator',
]);
// Шаблоны URI
Http::withUrlParameters([
'endpoint' => 'https://laravel.com',
'page' => 'docs',
'version' => '9.x',
'topic' => 'validation',
])->get('{+endpoint}/{page}/{version}/{topic}');
// Отправка файла
$response = Http::attach(
'attachment', file_get_contents('photo.jpg'), 'photo.jpg'
)->post('http://example.com/attachments');
// Повторные запросы
$response = Http::retry(3, 100)->post(/* ... */);
Но самое важное для нас это то, что мы получим при написании тестов. Так вот здесь становится жить гораздо легче. Можно сразу указать для какого URL какой ответ возвращать. В адресе можно использовать задание маски с помощью звездочек:
Http::fake([
// Stub a JSON response for GitHub endpoints...
'github.com/*' => Http::response(['foo' => 'bar'], 200, ['Headers']),
// Stub a string response for all other endpoints...
'*' => Http::response('Hello World', 200, ['Headers']),
]);
Можно описывать порядок следования запросов:
Http::fake([
// Stub a series of responses for GitHub endpoints...
'github.com/*' => Http::sequence()
->push('Hello World', 200)
->push(['foo' => 'bar'], 200)
->pushStatus(404),
]);
А более сложную логику можно задавать при помощи функций обратного вызова:
use Illuminate\Http\Client\Request;
Http::fake(function (Request $request) {
return Http::response('Hello World', 200);
});
Такой инструментарий сильно уменьшает рутину при написании автоматических тестов. А значит тестов успеем написать больше.
Если вы знаете какие-либо полезные инструменты при тестировании кода обращающегося к внешним ресурсам по HTTP, то напишите, пожалуйста, о них в комментариях.