| Как мы превратили Swagger из документации в двигатель API-автотестов |
| 22.07.2026 00:00 |
|
Автор: Олег Малышев, телеграмм-канал автора про QA,QA Auto, AI, Вайбкодинг лидер стека тестирования в компании «ТехВилл» Мы продолжаем разговор о том, как применять ИИ в тестировании. В этой статье расскажу, как мы пишем API-автотесты с помощью OpenAPI Generator, Cursor/Claude Code и автоматически считаем покрытие по Swagger через swagger-coverage. Раньше я уже записывал большое двухчасовое видео по Cursor, где показывал в том числе, как мы генерируем автотесты. Но с тех пор подход немного изменился: мы сильнее завязались на OpenAPI-контракт, добавили Swagger Coverage, JSON-отчёты для LLM и специальные skills для генерации недостающих тестов. Зачем всё это понадобилось Одна из типичных проблем API-автотестов: тесты живут отдельно от API-контракта. QA руками собирает URL, руками пишет JSON, руками парсит ответ. Swagger при этом существует где-то рядом: аналитики и разработчики его обновляют, тесты вроде бы проверяют API, но прямой связи между ними нет. Мы решили сделать Swagger не документацией «для галочки», а источником правды для API-тестов. На примере нашего сервиса Cohorts схема выглядит так: Тесты используют не ручные Как Swagger превращается в Java-клиентВозьмём endpoint создания когорты. В Swagger он описан как В OpenAPI-файле есть два уровня описания. Первый уровень -- Второй уровень - Оба фрагмента находятся в одном Swagger-файле. Первый - в разделе После генерации это превращается в Java-код: Если в Swagger есть поле И это принципиально: QA не придумывает DTO сам. Тест работает только с тем, что описано в контракте. Зачем нам Mustache-шаблоныOpenAPI Generator умеет генерировать код из коробки: в большинстве случаев стандартных шаблонов достаточно. Он сам создаст API-классы, модели, методы для path/query/body параметров и базовую обвязку клиента. Но если сгенерированный код не полностью подходит под стиль проекта, его можно донастроить через Mustache-шаблоны. Это обычный механизм OpenAPI Generator: мы можем взять стандартный шаблон и изменить то, как будут выглядеть generated API-классы или модели. В нашем случае мы используем Mustache-шаблоны не потому, что без них генерация невозможна, а потому что хотим, чтобы generated-код лучше ложился в наш тестовый фреймворк. Они подключаются в Gradle-задаче генерации:
api.mustache отвечает за генерацию API-классов: например, cohorts.api.CohortsApi. Именно через него задаётся форма generated-методов: А также методы выполнения запроса: и typed-десериализация ответа: То есть pojo.mustache отвечает за генерацию моделей: например, CreateCohortsRq, CreateCohortsRs, ListUsersWithPagination. В нём описывается, как должны выглядеть Java-классы моделей:
На практике это даёт нам возможность сохранить удобство генерации, но при этом контролировать форму generated-кода: Allure steps, fluent API, execute/executeAs, работу с RequestSpecification и стиль моделей. Как всё связывается через GradleТеперь соберём Gradle-часть: где скачивается Swagger, где генерируются Java-классы и как они попадают в тесты. В build.gradle подключён OpenAPI Generator: Generated-код подключён как test source set: Это значит, что Java-классы, которые OpenAPI Generator положит в build/generate-resources/src/main/java, будут доступны в тестах как обычный test-код. Для каждого сервиса есть две Gradle-задачи:
На примере Cohorts: Эта задача скачивает Swagger-файл в src/test/resources/cohorts.swagger.yaml. Иногда перед генерацией спецификацию приходится немного подготовить: привести её к виду, с которым OpenAPI Generator и coverage-инструмент работают предсказуемо. Такое бывает, если Swagger в сервисе исторически сложился неидеально: где-то не хватает единообразия, где-то используются слишком общие описания, где-то генератору не хватает конкретики. В статье не буду углубляться во внутренние детали такой подготовки. Важно другое: на вход генератору мы стараемся отдавать спецификацию, по которой можно стабильно получить Java-клиент и модели. Дальше запускается генерация: Эта задача берёт cohorts.swagger.yaml и генерирует: Чтобы всё это происходило автоматически перед компиляцией тестов, добавлена зависимость:
То есть когда в CI запускается Gradle test task, перед компиляцией тестов автоматически выполняется цепочка: Как generated client используется в тестахКак формируется cohortsDefaultApiВ тестах мы не создаём RestAssured-запрос каждый раз вручную. Для Cohorts есть базовый метод cohortsDefaultApi(), который возвращает generated API-класс cohorts.api.CohortsApi. Внутрь generated-клиента передаётся RequestSpecification. Он создаётся в CohortsSpec.defaultReqSpec(): Здесь есть несколько слоёв. Generated-клиент отвечает за то, что пришло из Swagger:
RequestSpecification отвечает за инфраструктуру конкретного тестового запуска:
Allure filter у нас тоже подключён в RequestSpecification: он добавляет HTTP-запросы и ответы в Allure-отчёт. Это удобно для разбора падений, но к самой генерации клиента из Swagger он не относится. Swagger Coverage filter нужен для другой задачи: он сохраняет фактические HTTP-вызовы в build/swagger-coverage-output, чтобы после тестов построить отчёт покрытия. Сам тест создания когорты Здесь почти каждая строка связана со Swagger: Path и query параметры тоже генерируютсяДругой пример: получение списка карт в когорте. В Swagger endpoint описан так: Из этого генерируется В тесте это выглядит так: QA не нужно помнить, как именно называется query-параметр в URL. Если параметр описан в Swagger, генератор создаёт для него отдельный метод. execute и executeAs: в чём разницаВ generated-клиенте есть два основных способа выполнить запрос:
Для нас это не просто удобная обёртка. Это дополнительная контрактная проверка. Если Swagger говорит, что ответ должен быть
Так мы постепенно приучаем команду держать контракт актуальным. На этом этапе у нас уже была написана большая пачка API-автотестов. Они использовали generated-клиенты, generated-модели и были ближе к Swagger-контракту, чем старые тесты с ручными URL и JSON. Но появилась другая проблема: мы перестали понимать, насколько эти тесты реально покрывают API. Тестов много, они проходят, но какие методы Swagger они вызывают? Какие endpoints вообще не тронуты? Какие покрыты только частично? Смотреть это руками по коду быстро стало неудобно. Поэтому мы выбрали другой подход: во время прогона автотестов собирать фактический «лог» HTTP-вызовов, а потом сравнивать его со Swagger/OpenAPI-спецификацией. Так мы пришли к Swagger Coverage: тесты продолжают выполняться как обычно, а после прогона мы получаем отчёт, который показывает, что именно из API-контракта было покрыто. Как Swagger Coverage работает в GitLab CIДля подсчёта покрытия мы используем доработанную версию swagger-coverage — инструмента, который изначально был создан Виктором Орловским: https://github.com/viclovsky/swagger-coverage Базовая идея библиотеки нам подошла: разделить процесс на два этапа. На первом этапе, во время выполнения автотестов, RestAssured filter собирает фактические HTTP-вызовы. На втором этапе, уже после тестового прогона, отдельная CI job сравнивает эти вызовы со Swagger/OpenAPI-спецификацией и строит отчёт покрытия. Оригинальный проект давно не дорабатывался, а в процессе интеграции мы столкнулись с ошибками и ограничениями. Поэтому Андрей Полетаев, @fenixnow, форкнул проект, поправил проблемы и адаптировал инструмент под наши задачи. Форк доступен здесь: В README форка подробнее описаны настройки и запуск через Docker: Во время тестов: SwaggerCoverage собирает вызовыВо время выполнения тестов RestAssured request specification содержит Это RestAssured filter. Он встраивается в цепочку выполнения HTTP-запроса и видит каждый запрос, который проходит через наш RequestSpecification. Важно: GitLab CI: тесты и сбор данных для coverageВ GitLab CI этот процесс разбит на два последовательных шага: сначала прогоняются API-тесты, затем на основе результатов тестового прогона автоматически собираются отчёты о покрытии. Во время тестового прогона Gradle запускает нужные test tasks. Перед компиляцией тестов он скачивает Swagger, генерирует Java-клиенты и модели, а затем запускает JUnit/RestAssured-тесты. Во время выполнения тестов SwaggerCoverage сохраняет фактические HTTP-вызовы в директорию:
После завершения тестов GitLab сохраняет эту директорию как artifact вместе со Swagger-файлами. Следующая job берёт эти данные и сравнивает: Swagger/OpenAPI specification и фактически выполненные HTTP-вызовы Именно из этой пары потом строится coverage report. GitLab CI: jobs для Swagger CoverageПосле тестов в пайплайне запускаются отдельные coverage jobs в Docker-образе с доработанным swagger-coverage:
В CI это выглядит как отдельный image для coverage job. Образ содержит swagger-coverage-commandline, поэтому job остаётся простой: ей нужно только передать спецификацию, input-директорию и конфиг. Для Cohorts команда выглядит так:
Здесь:
После этого job копирует результаты в public/cohorts:
На выходе получаем два артефакта:
HTML отчет выглядит следующим образом:
Как ссылки попадают в Merge RequestCoverage job сохраняет ссылки на HTML и JSON в dotenv artifact:
После этого отдельная job publish-coverage-report публикует комментарий в Merge Request. Она собирает ссылки по сервисам:
И пишет в MR сообщение со ссылками на два типа отчётов: HTML — человекочитаемый отчёт. Его удобно открыть в браузере: посмотреть сводку, методы, группы, варианты покрытия и быстро понять, где есть пробелы. LLM JSON — структурированный отчёт для модели. HTML красивый, но плохо подходит для автоматического анализа: его неудобно скармливать LLM и сложно стабильно разбирать. Поэтому в доработанной версии swagger-coverage мы добавили отдельный JSON-формат выгрузки результатов покрытия именно для LLM. В MR это выглядит так:
Вся CI-цепочка целикомВ CI мы используем Docker-образ с доработанной версией swagger-coverage. Его можно запустить как отдельный шаг пайплайна: на вход передаём Swagger/OpenAPI-файл, директорию с результатами, которые собрал SwaggerCoverage, и конфиг отчёта. В нашем случае CI-цепочка выглядит так: Именно эта связка делает процесс автоматическим. QA не нужно руками скачивать Swagger, запускать генератор, искать output coverage и собирать отчёт. Всё это делает pipeline, а на выходе команда получает понятный HTML-отчёт и JSON-файл, пригодный для анализа LLM. Как мы подключили LLM к покрытию APIКогда у нас появились Swagger Coverage отчёты, следующим логичным шагом стало использовать их не только как HTML-страницу для человека, но и как структурированный вход для LLM. Для этого в доработанной версии swagger-coverage мы сделали отдельный JSON-отчёт в формате, удобном для анализа моделью. В нём есть summary по покрытию, список paths, состояние каждой операции и requirements, которые ещё не закрыты тестами. В компании у нас кто-то работает в Cursor, кто-то в Claude Code. Поэтому мы описали правила генерации тестов в виде skills для обоих инструментов. Здесь важно, что skills разделены по ответственности. Первый skill анализирует coverage report и выступает оркестратором. Он понимает, какие endpoints не покрыты или покрыты частично, выбирает кандидатов для автотестов и передаёт задачу второму skill. Второй skill уже отвечает за написание Java/JUnit API-тестов по правилам проекта: где создать тест, какие generated methods и models использовать, когда выбрать executeAs, когда execute, какие assertions добавить и как оформить Allure steps. Первый skill: анализ coverage report и оркестрацияПервый skill получает на вход два файла:
Swagger-файл нужен, чтобы понять контракт API: endpoints, operationId, параметры, request body, response models и статусы. Coverage JSON — это LLM-friendly отчёт, который формируется в CI после тестового прогона. Его задача — не заменить HTML-отчёт для человека, а дать модели структурированные данные: что покрыто, что не покрыто и какие requirements ещё требуют тестов. Упрощённо он содержит: Skill анализирует этот JSON и понимает, какие операции находятся в каком состоянии:
Дальше он смотрит на requirements и определяет, что именно не покрыто:
На основе этого skill принимает решение, что делать дальше:
После анализа первый skill вызывает второй skill и передаёт ему уже не весь отчёт, а конкретную задачу: какой endpoint покрыть, какой operationId использовать, какие request/response schemas посмотреть и какие requirements закрыть.Второй skill: генерация тестов по правилам проектаВторой skill уже не занимается анализом всего coverage report. Он получает от первого skill конкретную задачу: какую операцию нужно покрыть и какие requirements нужно закрыть. Дальше он работает как исполнитель: смотрит Swagger/OpenAPI-спецификацию, находит нужный generated API method и generated models, проверяет существующие тесты и только после этого пишет автотест по правилам проекта. При генерации он следует нашим ограничениям:
Например, если coverage показывает, что не покрыт endpoint из Cohorts, skill идёт в Swagger, находит нужную операцию, смотрит operationId, request/response schema и понимает, какой generated-код надо использовать. Это важно: LLM не должна придумывать свой способ работы с API. Она должна пользоваться тем же контрактным клиентом, которым пользуются обычные тесты. Когда одного Swagger недостаточноВажно сказать честно: не каждый автотест можно сгенерировать только по Swagger и coverage report. Для простых CRUD-методов этого часто достаточно: есть endpoint, понятный request body, понятный response body, можно построить happy-path тест и базовые проверки. Но бывают сложные микросервисы, где сам вызов одного метода требует большого количества предусловий:
Swagger хорошо описывает контракт конкретного HTTP-метода, но он не всегда объясняет бизнес-контекст: откуда взять данные, в каком состоянии должна быть система, какие шаги нужно выполнить до вызова endpoint. В таких случаях нам нужны тест-кейсы, написанные QA. Там уже описаны предусловия, шаги, тестовые данные и ожидаемый результат. Для этого мы используем отдельные skills, которые работают не от coverage report, а от тест-кейсов в ТестОпс. Мы написали свой MCP для ТестОпс, через который LLM может получить тест-кейс, разобрать его и превратить в автотест. Так мы не пытаемся заменить QA и тест-дизайн одним Swagger. Swagger отвечает за контракт, coverage - за видимость пробелов, а тест-кейсы - за сложные бизнесовые сценарии, где без человеческого описания предусловий и ожидаемого поведения не обойтись. А что с gRPCВ этой статье все примеры намеренно взяты из REST API, где источником контракта является OpenAPI/Swagger. Для gRPC идея похожая: тесты тоже должны опираться на контракт. Но вместо Swagger там используются .proto-файлы, из которых генерируются service stubs, RPC methods и request/response messages. При этом техническая реализация отличается достаточно сильно: другой транспорт, другой формат контракта, другие generated-классы и другой подход к покрытию. Поэтому не будем смешивать всё в одной статье. Здесь говорим про REST, а gRPC-разбор оставим для отдельного материала. Как не перепутать generated-код и тестовый кодВ проекте есть два типа кода. Generated-код из Swagger: Его не пишем руками. Он появляется после генерации OpenAPI Generator. Наш тестовый код: BaseCohortTests Он отвечает за сценарии, шаги, проверки, токены, base URL и подключение фильтров. Если упростить разделение ответственности, получается так: То есть generated-код отвечает за технический контракт API, а наш тестовый код — за сценарий, данные, шаги и проверки. Всё, что связано с coverage и CI, живёт уровнем выше и уже было разобрано в отдельном разделе. Что это даётГлавная польза подхода в том, что автотесты становятся ближе к API-контракту. Если в Swagger поменяется operationId, изменится имя generated-метода. Если поменяется request body, изменится generated-модель. Если из ответа исчезнет поле, пропадёт getter. Многие расхождения становятся видны уже на этапе компиляции или десериализации, а не через неделю после релиза. Для QA тест при этом остаётся читаемым: По этой цепочке сразу видно:
Swagger Coverage закрывает вторую часть задачи: показывает, какие операции из Swagger реально были вызваны тестами, а какие ещё остались непокрытыми. LLM-skills закрывают третью часть: помогают превратить этот отчёт в новые автотесты, но не хаотично, а по правилам проекта. А тест-кейсы из ТестОпс закрывают сложные сценарии, где одного Swagger недостаточно и нужен бизнесовый контекст. В итоге Swagger перестаёт быть просто страницей с документацией. Он становится рабочим контрактом между аналитиками, разработчиками и QA, а coverage report - не просто красивым HTML-отчётом, а источником задач для дальнейшего развития автотестов. Что дальшеВ этой статье мы разобрали только один кусок большого процесса - API-автотесты от контракта и coverage report для LLM. В следующих материалах расскажем, как мы идём дальше: разбираем упавшие автотесты через дефекты в ТестОпс, связываем это с Яндекс Трекером и используем ИИ не только для генерации тестов, но и для анализа результатов и создания задач. Отдельно покажем, как строим систему, где ИИ подключается к нашим сервисам, сам готовит тест-планы, проверяет задачу через БД, REST и gRPC, а итоговый отчёт публикует прямо в задачу в Яндекс Трекере. Так что не переключайтесь - дальше будет ещё интереснее. |