OpenAPI — это спецификация, описывающая API-интерфейсы RESTful в форматах JSON и YAML так, что он понятен и людям, и машинам.
Определения OpenAPI не привязаны к конкретному языку и могут использоваться самым разным образом.
Определение OpenAPI можно использовать при генерировании документации для отображения API, в генераторах кода при создании серверов и клиентов на разных языках программирования, в средствах тестирования и в ряде других случаев. — Спецификация OpenAPI
В этой статье мы рассмотрим, как объединить определения OpenAPI 3.0.x с компоновочными тестами для проверки корректности работы API с помощью пакета OpenAPI HttpFoundation Testing.
Делать это мы будем на примере свежей установки Laravel, для которой мы также сгенерируем документацию Swagger UI в пакете L5 Swagger.
Сначала скажу пару слов о полезности такого подхода, но, если вы пришли сюда только за кодом, смело переходите к разделу Пример Laravel.
Проблема
API-интерфейсы стали привычными и распространенными, поэтому мы радуемся любой документации, помогающей добраться до всех конечных точек. Такая документация может различаться по форме и содержанию, но описания в ней должны меняться при любом изменении API.
Большинство разработчиков воспринимают поддержку документации по API как лишнюю домашнюю работу после того, как экзамен уже сдан; это скучное, утомительное и обычно неблагодарное занятие. Тут может помочь использование аннотаций, хранящих код и документацию в одном месте; но писать их зачастую утомительно, и даже самый прилежный разработчик может упустить что-то, а его коллеги не заметят этого.
В результате API-интерфейс и его документация перестанут совпадать, вводя пользователей в заблуждение.
Другой аспект поддержки API состоит в том, чтобы ни одна конечная точка не перестала работать должным образом, — ухудшение происходит постепенно и может долго оставаться незамеченным без должной стратегии тестирования.
Во избежание этого можно реализовать компоновочное тестирование, автоматически проверяющее корректность работы API и позволяющее убедиться, что недавние изменения не привели к непредсказуемым последствиям. Однако где гарантия, что ожидаемые результаты компоновочного тестирования точно соответствуют тем, которые приведены в документации?
Хорошо бы найти способ убедиться, что они абсолютно одинаковы...
Решение
Допустим, что у нас есть документация на API и компоновочные тесты, а теперь нам нужно как-то согласовать результаты.
Спецификация OpenAPI стала популярным способом описывать API-интерфейсы по мере их изменения, но даже она не избавит нас от необходимости поддерживать соответствующие определения. Другими словами, даже OpenAPI не гарантирует, что все идет как надо.
Однако OpenAPI отличается от других решений тем, что его можно использовать как фундамент для создания растущего числа инструментов, которые позволяют получить от спецификации намного больше пользы, чем просто документирование.
Один из таких инструментов, написанный для экосистемы PHP и поддерживаемый командой The PHP League, называется OpenAPI PSR-7 Message Validator. Это пакет валидации HTTP-запросов и ответов на соответствие определениям OpenAPI использует стандарт PSR-7.
По сути, каждый HTTP-запрос и ответ проверяется на соответствие хотя бы одной операции, описанной в определении OpenAPI.
Понимаете, к чему я веду?
Нам достаточно использовать этот пакет в качестве дополнительного слоя поверх наших компоновочных тестов, чтобы он проверял получаемые тестами API-ответы на соответствие определениям OpenAPI описывающим API.
Если соответствие отсутствует, считается, что тест не пройден.
Вот как это выглядит на схеме:
(Работа автора)
Определение OpenAPI описывает API, и тесты используют его для проверки того, что API ведет себя именно так, как сказано в определении.
В результате определение OpenAPI становится базой как для кода, так и для тестов, являясь единым источником достоверной информации об API.
PSR-7
Наверняка вы заметили небольшой нюанс в предыдущем разделе: пакет OpenAPI PSR-7 Message Validator работает только с сообщениями PSR-7, о чем говорит его название. Проблема состоит в том, что не все платформы поддерживают этот стандарт изначально. Более того, многие из них используют компонент HttpFoundation платформы Symfony, запросы и ответы которого по умолчанию не реализуют этот стандарт.
Однако команда разработчиков Symfony прикрыла нам тылы, разработав мост, конвертирующий объекты HttpFoundation в объекты PSR-7, если есть подходящая фабрика PSR-7 и PSR-17, в качестве которой они предлагают использовать созданную Тобиасом Нихолмом (Tobias Nyholm) реализацию PSR-7.
Собрав все нужные части головоломки, предлагаемые пакетом OpenAPI HttpFoundation Testing, разработчики могут подкрепить свои компоновочные тесты определениями OpenAPI в проектах, использующих компонент HttpFoundation.
Давайте посмотрим, как это делается в проекте Laravel, подпадающем под эту категорию.
Пример Laravel
Приведенный в этом разделе код также доступен в виде репозитория GitHub.
Сначала создадим с помощью Composer новый проект Laravel 8:
$ composer create-project --prefer-dist laravel/laravel openapi-example "8.*"
Введем корневую папку проекта и установим ряд подчиненных пакетов:
$ cd openapi-example
$ composer require --dev osteel/openapi-httpfoundation-testing
$ composer require darkaonline/l5-swagger
Первый — это упомянутый ранее пакет OpenAPI HttpFoundation Testing, который мы ставим как подчиненный пакет среды разработки, поскольку мы планируем использовать его как часть набора тестов.
Второй — это популярный пакет L5 Swagger, создающий мост между Laravel и платформами Swagger PHP и Swagger UI. На самом деле пакет Swagger PHP не требуется, так как он использует для генерирования определений OpenAPI аннотации Doctrine, а мы планируем писать аннотации вручную. Но вот пакет Swagger UI нам нужен и легко адаптируется для работы с Laravel.
Чтобы пакет Swagger PHP не перезаписывал определение OpenAPI, зададим в файле, .env
в корневой папке проекта, следующую переменную среды
L5_SWAGGER_GENERATE_ALWAYS=false
Создадим файл с именем api-docs.yaml
в созданной нами папке storage/api-docs
и добавим в него следующее содержимое:
openapi: 3.0.3
info:
title: OpenAPI HttpFoundation Testing Laravel Example
version: 1.0.0
servers:
- url: http://localhost:8000/api
paths:
'/test':
get:
responses:
'200':
description: Ok
content:
application/json:
schema:
type: object
required:
- foo
properties:
foo:
type: string
example: bar
Это простое определение OpenAPI, описывающее единичную операцию, — GET
Запрос конечной точки /api/test
, который должен возвращать JSON-объект с требуемым ключом foo
.
Проверим, корректно ли отображает Swagger UI наше определение OpenAPI. Запустим сервер PHP-разработки, введя команду artisan
в корневой папке проекта:
$ php artisan serve
Откройте путь localhost:8000/api/documentation в браузере и замените api-docs.json
на api-docs.yaml
в верхней строке навигации (чтобы Swagger UI загружал определение YAML вместо JSON, которого у нас не будет).
Нажмите клавишу Enter или щелкните кнопку Explore (Обзор) — наше определение OpenAPI должно быть сгенерировано в виде документации Swagger UI:
Разверните конечную точку /test
и попробуйте ее открыть — должна появиться ошибка 404 Not Found
, поскольку мы пока ее не реализовали.
Исправим это. Откройте файл routes/api.php
и измените пример маршрута на следующий:
Route::get('/test', function (Request $request) {
return response()->json(['foo' => 'bar']);
});
Вернитесь на вкладку Swagger UI и снова попробуйте конечную точку — теперь она должна возвратить успешный ответ.
Перейдем к написанию теста! Откройте файл tests/Feature/ExampleTest.php
и замените его содержимое на следующее:
<?php
namespace Tests\Feature;
use Osteel\OpenApi\Testing\ValidatorBuilder;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*
* @return void
*/
public function testBasicTest()
{
$response = $this->get('/api/test');
$validator = ValidatorBuilder::fromYaml(storage_path('api-docs/api-docs.yaml'))->getValidator();
$result = $validator->validate($response->baseResponse, '/test', 'get');
$this->assertTrue($result);
}
}
Давайте слегка рассмотрим его. Для тех, кто не знает, Laravel $this->get()
представляет собой метод тестирования, предоставляемый трейтом MakesHttpRequests, который по сути выполняет запрос GET к указанной конечной точке внутри приложения. Он возвращает ответ, идентичный тому, который мы получили бы при выполнении этого же запроса извне.
Затем мы создаем валидатор с помощью класса Osteel\OpenApi\Testing\ResponseValidatorBuilder
, которому мы передаем написанное ранее определение YAML с помощью статического метода fromYaml
(функция storage_path
возвращает путь к папке storage
, в которой хранится определение).
Если бы мы работали с определением JSON, мы бы использовали метод fromJson
; кроме того, оба метода принимают как строки, так и файлы YAML и JSON.
Построитель возвращает экземпляр Osteel\OpenApi\Testing\ResponseValidator
, в котором мы вызываем метод GET, передавая в виде параметров путь и ответ ($response
представляет собой объект-оболочку Illuminate\Testing\TestResponse
для внутреннего объекта HttpFoundation
, к которому можно обратиться через общедоступное свойство baseResponse
).
Этот код как бы говорит: «Я хочу убедиться, что этот ответ соответствует определению OpenAPI для GET запроса пути /test».
Он может быть написан следующим образом:
$result = $validator->get('/test', $response->baseResponse);
Это допустимо, поскольку валидатор поддерживает сокращения для каждого HTTP-метода, поддерживаемого OpenAPI (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS и TRACE), чтобы упростить тестирование ответов для соответствующих операций.
Учтите, что указанный путь должен точно соответствовать одному из путей определения OpenAPI.
Теперь можно запустить тест, который завершится успешно:
$ ./vendor/bin/phpunit tests/Feature
Снова откройте файл routes/api.php
и измените маршрут на следующий:
Route::get('/test', function (Request $request) {
return response()->json(['baz' => 'bar']);
});
Снова запустите тест. Теперь он должен завершиться неудачей, так как ответ содержит baz
вместо foo
, а согласно определению OpenAPI ожидается последнее.
Наш тест официально поддерживается OpenAPI!
Разумеется, это очень упрощенный пример, предназначенный лишь для демонстрации, а в реальной ситуации будет правильно переписать MakesHttpRequests
метод call трейта, чтобы он выполнял как запрос теста, так и валидацию согласно OpenAPI.
В результате наш тест уместится в одну строку:
$this->get('/api/test');
Его можно реализовать в виде нового трейта MakesOpenApiRequests
, который «расширит» трейт MakesHttpRequests
, в результате чего сначала будет вызывать родительский метод call
для получения ответа. Он будет извлекать путь из URI-адреса и проверять ответ на соответствие определению OpenAPI, прежде чем возвращать его, а затем вызывать тест, чтобы выполнить дополнительные проверки.
Заключение
Описанный выше подход значительно повышает надежность API, но не является универсальным решением — компоновочные тесты должны охватывать каждую отдельную конечную точку, что не так-то просто автоматизировать, а разработчики все равно должны оставаться дисциплинированными и внимательными. Поначалу это даже может быть воспринято как принуждение, так как разработчиков по сути заставляют поддерживать документацию, чтобы писать успешные тесты.
Но в результате документация гарантированно станет более точной, а пользователи будут наслаждаться меньшим числом ошибок, связанных с API, что в свою очередь уменьшит недовольство разработчиков, которым придется тратить меньше времени на поиск ляпов.
Сделав определения OpenAPI единым мерилом достоверности как для документации на API, так и для компоновочных тестов, мы мотивируем разработчиков поддерживать их актуальность, что естественным образом станет приоритетом.
Что же касается поддержки самих определений OpenAPI, делать это вручную — более сложная задача. Можно использовать аннотации, но я предпочитаю поддерживать файл YAML напрямую. Различные IDE-расширения, например это для VSCode, заметно упрощают эту задачу, но если сам вид файла YAML или JSON вызывает у вас отвращение, вы можете работать с ним, используя приятный интерфейс специальных инструментов, таких как Stoplight Studio.
Раз уж мы упомянули Stoplight*, могу порекомендовать статью С чего начать, с проектирования API или с кода (API Design-First vs Code First), написанную Филом Стердженом (Phil Sturgeon), как отличную отправную точку в документировании API, которая поможет выбрать тот подход, который вам по душе.
* Я не имею никакого отношения к Stoplight.
Ресурсы
Перевод статьи подготовлен в рамках курса "Framework Laravel". Автор оригинала Yannick Chenot. Если вам интересно узнать о формате обучения и программе курса подробнее, приглашаем на день открытых дверей онлайн.
• РЕГИСТРАЦИЯ •