Swagger - отличный способ динамического документирования веб-API. Тем не менее, я видел большую часть неработающих настроек Swagger, таких как невозможность создания всей документации из-за недопустимой аннотации атрибута или метода общедоступного контроллера, который не был загружен в метод HTTP.

Вдобавок к этому легко забыть обновить статическую конфигурацию Swagger, такую ​​как версия, заголовок, описание и файл документации XML. Однако, чтобы убедиться, что мы не неверно сконфигурируем конфигурацию Swagger каким-либо образом, мы должны автоматически протестировать всю настройку Swagger, убедившись, что наша конечная точка Swagger успешно работает с ожидаемой конфигурацией.

В этом сообщении блога будет представлена ​​«FluentSwaggerTests» - способ динамической проверки настройки Swagger с помощью автоматических интеграционных тестов.

Хрупкость конфигураций Swagger

Во-первых, давайте рассмотрим причины, почему так важно автоматически тестировать любую конечную точку Swagger.

Swagger великолепен в том смысле, что он генерирует документацию на основе кода, который мы пишем, так что документация API всегда отражает функциональность в нашем коде. Однако эта автоматически сгенерированная документация - очень хрупкая операция, которая легко завершится неудачей, если мы введем в наш код неподдерживаемые операции. И даже хуже; обычно он завершается с ошибкой (!), что означает, что наш компонент компилируется и запускается без каких-либо ошибок, пока мы явно не отправим запрос в нашу конечную точку Swagger.

Чтобы проиллюстрировать одну из этих скрытых неправильных конфигураций конечной точки Swagger, представьте, что у нас есть контроллер для проверки связи нашего API, чтобы убедиться, что он запущен и работает:

Когда мы запустим наш компонент и перейдем к нашей конечной точке Swagger, мы увидим это:

Это удачная конфигурация Swagger, и вроде все нормально.

Теперь представим, что мы добавляем к нашему контроллеру публичный метод без какого-либо атрибута HTTP-метода, такого как атрибут [HttpGet]. Вы можете спросить «почему?» - ну, может быть, у нас есть собственный класс ControllerBase, от которого все наши контроллеры наследуют функциональность, например, совместное ведение журнала. Для краткости этого примера давайте добавим такой общедоступный метод в наш PingController:

Теперь давайте попробуем снова запустить наш API:

Отлично - никаких ошибок компиляции или выполнения! Теперь давайте снова загрузим нашу конечную точку Swagger:

Конечная точка Swagger теперь не работает. Если мы посмотрим на журналы консоли для нашего компонента, теперь мы сможем увидеть, что вызвало ошибку:

Конечная точка Swagger не загружается из-за того, что для метода PingController.LogInformation требуется «явная привязка HttpMethod». Здесь следует отметить тревожный аспект: когда мы запускали наш компонент, все выглядело нормально; ошибок, указывающих на неверно настроенную конечную точку Swagger, не было. Эта ошибка возникла только тогда, когда мы явно отправили запрос в нашу конечную точку Swagger, что означает, что конфигурация Swagger не проверяется при запуске компонента.

Другой способ незаметно сломать конечную точку Swagger - настроить неверный путь к сгенерированному файлу документации Swagger. В качестве примера возьмем конфигурацию FluentSwagger:

Эта конфигурация Swagger действительна, и конечная точка может найти нашу сгенерированную API-документацию в DocUrl, указанном в файле конфигурации.

Однако давайте изменим DocUrl на что-нибудь другое, в этом случае он начинается с «/ swaggerdoc /» вместо «/ swagger /»:

Теперь давайте снова запустим наш компонент. Вроде все работает без ошибок! Однако, если мы снова перезагрузим нашу конечную точку Swagger, мы получим следующую ошибку:

Еще одна тихая ошибка от Swagger, и на этот раз в консоли вообще нет ошибок:

Он не только тихо выходит из строя при запуске компонента, он даже не выходит из строя, когда ошибка возникает в реальном времени! В фоновом режиме возвращается ответ HTTP 404 (NotFound), который также записывается в консоль, но нет четких журналов ошибок, указывающих, что что-то настроено неправильно.

«FluentSwaggerTests» спешит на помощь

Мы рассмотрели способы сломать установку Swagger, что в какой-то момент, вероятно, случилось со всеми нами. Наиболее тревожным аспектом этих неправильных конфигураций является то, что они тихо терпят неудачу и их трудно обнаружить на ранних этапах цикла разработки.

Чтобы избежать неправильной конфигурации конечных точек Swagger в будущем, в идеале было бы иметь тест интеграции, который проверяет, что конечная точка Swagger работает с ожидаемой конфигурацией и что ошибок не возникает. Я рад представить «FluentSwaggerTests», который позволяет легко провести такой интеграционный тест с минимальной конфигурацией и настройкой.

При использовании «FluentSwaggerTests» интеграционный тест для проверки успешно настроенной конечной точки Swagger будет следующим:

Как видите, в этом тесте используется WebApplicationFactory для запуска нашего API в качестве полнофункционального веб-сервера, на котором мы можем выполнять тесты. Это быстрый и автономный веб-сервер, настроенный для выполнения интеграционных тестов.

В тестовом методе «VerifySwaggerIsRunningCorrectly_ServerWithSwaggerSetup_SwaggerIsRunningCorrectly» мы сначала объявляем экземпляр нашего класса «SwaggerRuntimeInformation», который содержит ожидаемую конфигурацию для нашей конечной точки Swagger. Это простой DTO, который содержит только те поля, которые мы указываем в этом тесте. Эти поля также отражают свойства конфигурации Swagger нашего конфигурационного файла «appsettings.json».

Для ожидаемого свойства «Version» класса «SwaggerRuntimeInformation» мы получаем его динамически через нашу сборку производственного кода в методе «GetProductionVersion». Таким образом, нам не нужно адаптировать наш тест каждый раз, когда выпускается новая минорная версия или версия исправления / патча. Однако, если будет выпущена новая основная версия, то тест прервется из-за ожидаемого «VersionName», которое необходимо изменить на новую основную версию. Если вы хотите, вы также можете динамически извлечь эту основную версию из версии, возвращенной в «GetProductVersion», но я решил установить порог ошибки теста при изменении основной версии.

Экземпляр _swaggerRuntimeValidator класса SwaggerRuntimeValidator - это класс, который будет проверять правильность работы конечной точки Swagger. Мы передаем HttpClient, который был настроен с базовым путем нашего экземпляра TestServer, который WebApplicationFactory настроил для нас, а «SwaggerDocPath» - это ожидаемый путь к нашему сгенерированному файлу документации Swagger (который настроен в Swagger.DocUrl в нашем конфигурационном файле «appsettings.json»).

Давайте посмотрим на класс SwaggerRuntimeValidator:

Единственный общедоступный метод VerifySwaggerIsRunningCorrectly - это метод, который вызывает наш интеграционный тест. Этот метод извлекает информацию о времени выполнения Swagger из предоставленного нами пути Swagger DocUrl и гарантирует, что ожидаемая информация о времени выполнения Swagger, которую мы передали в качестве аргумента методу, совпадает с фактической информацией о времени выполнения, извлеченной из Swagger DocUrl. Это утверждение будет перехватывать исключения, если какая-либо из конфигураций конечной точки Swagger отличается от ожидаемых, например, если версии, заголовки или описания не совпадают.

Теперь, если мы углубимся в стек вызовов и посмотрим на метод «ExtractSwaggerRuntimeInfo», код будет делать следующее:

  • Отправьте HTTP-запрос GET в Swagger DocUrl
  • Убедитесь, что ответ - HTTP OK (200)
  • Десериализовать содержимое ответа HTTP в экземпляр класса SwaggerEndpointConfig, который также является DTO, содержащим поля, которые можно извлечь из конечной точки Swagger.
  • Убедитесь, что SwaggerEndpointConfig равен ожидаемому SwaggerRuntimeInfo.

Примечание. В методе «BuildSwaggerRuntimeInfoFromEndpoint» мы видим, что сопоставление между классом «SwaggerEndpointConfig» и классом «SwaggerRuntimeInformation», которое возвращает метод, является однозначным (точное одинаковые свойства), но семантически эти два объекта все еще не одно и то же, поэтому, на мой взгляд, правильным является разделение для них классов.

Теперь метод «DeserializeSwaggerJsonConfig» для извлечения конфигурации конечной точки Swagger из фактической конечной точки Swagger просто считывает содержимое ответа HTTP, десериализует его в экземпляр класса «SwaggerEndpointConfig» и возвращает его. Если десериализация завершится неудачно, либо если выбрасывается исключение, либо если после десериализации экземпляр swaggerEndpointConfig все еще имеет значение null, будет выброшено исключение. Эта функция будет обнаружена, если наша конечная точка Swagger каким-либо образом настроена неправильно, например, если произойдет одна из неправильных конфигураций, на которые мы обращали внимание ранее.

Более того, наша интеграция обнаружит любую из потенциальных неправильных конфигураций нашей конечной точки Swagger и обеспечит соответствие конфигураций нашим ожиданиям, таким как версия, имя версии, заголовок и описание. Таким образом, нам никогда не придется беспокоиться о том, что мы случайно сломаем конечную точку Swagger, не зная об этом.

Заключительные слова

В этом сообщении в блоге рассказывается о хрупкости конфигураций Swagger и о том, как легко их сломать, даже не зная об этом. «FluentSwaggerTests» решает эти потенциальные проблемы, предлагая автономные тесты интеграции, которые проверяют, что любая конечная точка Swagger настроена должным образом и что при взаимодействии с конечной точкой не возникает ошибок.

При желании вы можете проверить и использовать Код FluentSwaggerTests для себя - он находится в тестовом проекте репозитория FluentSwagger.

Спите лучше, зная, что вы больше никогда не нарушите свою конфигурацию Swagger!