惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
D
Docker
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
J
Java Code Geeks
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Simon Willison's Weblog
Simon Willison's Weblog
S
Security Affairs
NISL@THU
NISL@THU
T
Tor Project blog
A
About on SuperTechFans
宝玉的分享
宝玉的分享
腾讯CDC
S
Schneier on Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Privacy & Cybersecurity Law Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Stack Overflow Blog
Stack Overflow Blog
P
Privacy International News Feed
雷峰网
雷峰网
C
Cyber Attacks, Cyber Crime and Cyber Security
Vercel News
Vercel News
Cisco Talos Blog
Cisco Talos Blog
D
DataBreaches.Net
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Google Online Security Blog
Google Online Security Blog
Recorded Future
Recorded Future
L
LINUX DO - 热门话题
Microsoft Security Blog
Microsoft Security Blog
Latest news
Latest news
C
Check Point Blog
有赞技术团队
有赞技术团队
T
The Exploit Database - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
云风的 BLOG
云风的 BLOG
SecWiki News
SecWiki News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
爱范儿
爱范儿
月光博客
月光博客
V
Vulnerabilities – Threatpost
T
Threat Research - Cisco Blogs
P
Palo Alto Networks Blog
T
The Blog of Author Tim Ferriss
C
Cisco Blogs
Webroot Blog
Webroot Blog
S
Security @ Cisco Blogs

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как мы превратили Swagger из документации в двигатель API-автотестов
Олег Малышев · 2026-06-24 · via Все публикации подряд на Хабре

Всем привет! Меня зовут Олег Малышев. Я один из лидеров стека тестирования в компании «ТехВилл»

Мы продолжаем разговор о том, как применять ИИ в тестировании. В этой статье расскажу, как мы пишем 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 схема выглядит так:

cohorts.swagger.yaml
        ↓
OpenAPI Generator + Mustache templates
        ↓
cohorts.api.CohortsApi
cohorts.model.*
        ↓
JUnit + RestAssured tests
        ↓
Swagger Coverage
        ↓
HTML / JSON отчёт покрытия
        ↓
LLM-skills для генерации недостающих тестов

Тесты используют не ручные given().get("/api/..."), а generated-клиент и generated-модели, созданные из OpenAPI-спецификации.

Как Swagger превращается в Java-клиент

Возьмём endpoint создания когорты. В Swagger он описан как POST /api/v1/cohort.

В OpenAPI-файле есть два уровня описания.

Первый уровень -- paths. Он говорит, какой endpoint вызвать, каким HTTP-методом, какой operationId у операции, какую request-схему нужно отправить и какую response-схему ожидаем получить.

/api/v1/cohort:
  post:
    tags:
      - cohorts
    summary: Cоздание пустой когорты
    operationId: createCohort
    requestBody:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/CreateCohortsRq'
    responses:
      201:
        description: Успешно
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCohortsRs'

Второй уровень - components.schemas. Там описано, из каких полей состоят DTO-модели, на которые endpoint ссылается через $ref.

CreateCohortsRq:
  type: object
  required:
    - name
    - description
  properties:
    name:
      type: string
      description: Название когорты
    description:
      type: string
      description: Описание когорты
    case_type:
      type: integer
      description: Тип использования когорты

CreateCohortsRs:
  type: object
  required:
    - id
  properties:
    id:
      type: integer
      description: ID когорты

Оба фрагмента находятся в одном Swagger-файле. Первый - в разделе paths, где описаны endpoint’ы. Второй - в разделе components.schemas, где описаны DTO-модели.

После генерации это превращается в Java-код:

operationId: createCohort → cohortsDefaultApi().createCohort()
CreateCohortsRq           → cohorts.model.CreateCohortsRq
CreateCohortsRs           → cohorts.model.CreateCohortsRs

Если в Swagger есть поле name, в Java-модели есть builder-метод .name(...). Если в Swagger есть поле id, в response-модели есть getId().

И это принципиально: QA не придумывает DTO сам. Тест работает только с тем, что описано в контракте.

Зачем нам Mustache-шаблоны

OpenAPI Generator умеет генерировать код из коробки: в большинстве случаев стандартных шаблонов достаточно. Он сам создаст API-классы, модели, методы для path/query/body параметров и базовую обвязку клиента.

Но если сгенерированный код не полностью подходит под стиль проекта, его можно донастроить через Mustache-шаблоны. Это обычный механизм OpenAPI Generator: мы можем взять стандартный шаблон и изменить то, как будут выглядеть generated API-классы или модели.

В нашем случае мы используем Mustache-шаблоны не потому, что без них генерация невозможна, а потому что хотим, чтобы generated-код лучше ложился в наш тестовый фреймворк.

src/test/resources/openapi-templates/api.mustache
src/test/resources/openapi-templates/pojo.mustache

Они подключаются в Gradle-задаче генерации:

templateDir.set("$rootDir/src/test/resources/openapi-templates")

api.mustache отвечает за генерацию API-классов: например, cohorts.api.CohortsApi.

Именно через него задаётся форма generated-методов:

public CreateCohortOper createCohort() {
    return new CreateCohortOper(createReqSpec());
}

А также методы выполнения запроса:

public <T> T execute(Function<Response, T> handler) {
    return handler.apply(
            RestAssured.given()
                    .spec(
reqSpec.build())
                    .expect()
                    .spec(
respSpec.build())
                    .when()
                    .request(REQ_METHOD, REQ_URI)
    );
}

и typed-десериализация ответа:

public CreateCohortsRs executeAs(Function<Response, Response> handler) {
    TypeRef<CreateCohortsRs> type = new TypeRef<CreateCohortsRs>() {};
    return execute(handler).as(type);
}

То есть execute(...), executeAs(...), .idPath(...), .pageQuery(...), .body(...) - это не магия RestAssured сама по себе. Это форма generated-клиента, которую мы получаем через OpenAPI Generator и наши шаблоны.

pojo.mustache отвечает за генерацию моделей: например, CreateCohortsRq, CreateCohortsRs, ListUsersWithPagination.

В нём описывается, как должны выглядеть Java-классы моделей:

поля из Swagger schema
Jackson-аннотации
getters/setters
builder-style методы
equals/hashCode/toString

На практике это даёт нам возможность сохранить удобство генерации, но при этом контролировать форму generated-кода: Allure steps, fluent API, execute/executeAs, работу с RequestSpecification и стиль моделей.

Как всё связывается через Gradle

Теперь соберём Gradle-часть: где скачивается Swagger, где генерируются Java-классы и как они попадают в тесты.

В build.gradle подключён OpenAPI Generator:

plugins {
    id "org.openapi.generator" version "7.18.0"
}

Generated-код подключён как test source set:

sourceSets {
    test {
        java {
            srcDir('build/generate-resources/src/main/java')
        }
    }
}

Это значит, что Java-классы, которые OpenAPI Generator положит в build/generate-resources/src/main/java, будут доступны в тестах как обычный test-код.

Для каждого сервиса есть две Gradle-задачи:

downloadCohortsSwaggerFile → скачать и подготовить Swagger
openApiCohorts             → сгенерировать Java RestAssured client

На примере Cohorts:

tasks.register('downloadCohortsSwaggerFile', Exec) {
    group = "openapi tools"

    def url = "

https://example.com/cohort/openapi_cohort.yaml"
    def outputFile = file("${projectDir}/src/test/resources/cohorts.swagger.yaml")

    if (outputFile.exists()) {
        commandLine("curl", "-z", outputFile, "-o", outputFile, url)
    } else {
        commandLine("curl", "-o", outputFile, url)
    }
}

Эта задача скачивает Swagger-файл в src/test/resources/cohorts.swagger.yaml.

Иногда перед генерацией спецификацию приходится немного подготовить: привести её к виду, с которым OpenAPI Generator и coverage-инструмент работают предсказуемо. Такое бывает, если Swagger в сервисе исторически сложился неидеально: где-то не хватает единообразия, где-то используются слишком общие описания, где-то генератору не хватает конкретики.

В статье не буду углубляться во внутренние детали такой подготовки. Важно другое: на вход генератору мы стараемся отдавать спецификацию, по которой можно стабильно получить Java-клиент и модели.

Дальше запускается генерация:

tasks.register("openApiCohorts", GenerateTask) {
    dependsOn 'downloadCohortsSwaggerFile'

    apiPackage.set("cohorts.api")
    modelPackage.set("cohorts.model")
    generatorName = "java"
    library = "rest-assured"

    inputSpec.set("$rootDir/src/test/resources/cohorts.swagger.yaml")
    outputDir.set("$buildDir/generate-resources")

    templateDir.set("$rootDir/src/test/resources/openapi-templates")
}

Эта задача берёт cohorts.swagger.yaml и генерирует:

cohorts.api.CohortsApi
cohorts.ApiClient
cohorts.model.CreateCohortsRq
cohorts.model.CreateCohortsRs
cohorts.model.ListUsersWithPagination
...

Чтобы всё это происходило автоматически перед компиляцией тестов, добавлена зависимость:

compileTestJava.dependsOn tasks.openApiBanners, tasks.openApiGifts, tasks.openApiCohorts

То есть когда в CI запускается Gradle test task, перед компиляцией тестов автоматически выполняется цепочка:

downloadCohortsSwaggerFile
        ↓
openApiCohorts
        ↓
compileTestJava
        ↓
test

Как generated client используется в тестах

Как формируется cohortsDefaultApi

В тестах мы не создаём RestAssured-запрос каждый раз вручную. Для Cohorts есть базовый метод cohortsDefaultApi(), который возвращает generated API-класс cohorts.api.CohortsApi.

public class BaseCohortTests {

   protected CohortsApi cohortsDefaultApi() {

       return ApiClient.api(ApiClient.Config.apiConfig().reqSpecSupplier(

               () -> new RequestSpecBuilder()

                       .addRequestSpecification(CohortsSpec.defaultReqSpec())

       )).cohorts();

   }

}

Внутрь generated-клиента передаётся RequestSpecification. Он создаётся в CohortsSpec.defaultReqSpec():

public static RequestSpecification defaultReqSpec() {

   return new RequestSpecBuilder()

           .setBaseUri(config.COHORTS_URL())

           .setConfig(Specifications.hiddenTokensConfig())

           .addFilter(new AllureRestAssured())

           .addFilter(new SwaggerCoverageV3RestAssured(

                   new FileSystemOutputWriter(Paths.get("build/swagger-coverage-output/cohorts"))))

           .addHeader("x-vkusvill-token", valuesConfig.cohortsToken())

           .addHeader("x-vkusvill-device", "ekb")

           .log(LogDetail.ALL)

           .build();

}

Здесь есть несколько слоёв.

Generated-клиент отвечает за то, что пришло из Swagger:

endpoint
HTTP method
path parameters
query parameters
request body
response model

RequestSpecification отвечает за инфраструктуру конкретного тестового запуска:

base URL
токены
технические заголовки
логирование
Allure filter
Swagger Coverage filter

Allure filter у нас тоже подключён в RequestSpecification: он добавляет HTTP-запросы и ответы в Allure-отчёт. Это удобно для разбора падений, но к самой генерации клиента из Swagger он не относится.

Swagger Coverage filter нужен для другой задачи: он сохраняет фактические HTTP-вызовы в build/swagger-coverage-output, чтобы после тестов построить отчёт покрытия.

Сам тест создания когорты

@Test
@DisplayName("Создание пустой когорты")
void createCohort() {
    CreateCohortsRq request = CreateCohortsRq.builder()
            .name("Автотестовая когорта " + UUID.randomUUID())
            .description("Когорта создана автотестом")
            .build();

    CreateCohortsRs result = cohortsDefaultApi()
            .createCohort()
            .body(request)
            .executeAs(response -> response
                    .then()
                    .statusCode(201)
                    .extract().response());

    cohortId = result.getId();

    step("И я проверяю, что когорта создана", () -> {
        assertNotNull(result, "Ответ не должен быть null");
        assertNotNull(cohortId, "ID созданной когорты не должен быть null");
        assertNotEquals(0, cohortId, "ID созданной когорты не должен быть 0");
    });
}

Здесь почти каждая строка связана со Swagger: CreateCohortsRq - request model, createCohort() - operationId, body(request) - request body, CreateCohortsRs - response schema.

Path и query параметры тоже генерируются

Другой пример: получение списка карт в когорте.

В Swagger endpoint описан так:

/api/v1/cohort/{id}/users:
  get:
    operationId: getUsers
    parameters:
      - in: path
        name: id
      - in: query
        name: page
      - in: query
        name: size
      - in: query
        name: search_pattern
    responses:
      200:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListUsersWithPagination'

Из этого генерируется fluent API:

operationId: getUsers
        → .getUsers()

path parameter id
        → .idPath(...)

query parameter page
        → .pageQuery(...)

query parameter size
        → .sizeQuery(...)

query parameter search_pattern
        → .searchPatternQuery(...)

response schema ListUsersWithPagination
        → ListUsersWithPagination result

В тесте это выглядит так:

@Test
@DisplayName("Получение списка карт в когорте")
void getUsers() {
    step("Дано ID когорты: %s".formatted(cohortId));

    ListUsersWithPagination result = cohortsDefaultApi()
            .getUsers()
            .idPath(cohortId)
            .pageQuery(PAGE)
            .sizeQuery(PAGE_SIZE)
            .searchPatternQuery(number)
            .executeAs(response -> response
                    .then()
                    .statusCode(200)
                    .extract().response());

    step("И я проверяю тело ответа", () -> {
        assertNotNull(result, "Ответ не должен быть null");
        assertNotNull(result.getNumbers(), "Список карт не должен быть null");
        assertTrue(result.getNumbers().contains(number), "Список карт должен содержать тестовую карту");
        assertNotNull(result.getPageNumber(), "Поле pageNumber не должно быть null");
        assertNotNull(result.getPageSize(), "Поле pageSize не должно быть null");
        assertNotNull(result.getTotalPages(), "Поле totalPages не должно быть null");
        assertNotNull(result.getTotalCount(), "Поле totalCount не должно быть null");
    });
}

QA не нужно помнить, как именно называется query-параметр в URL. Если параметр описан в Swagger, генератор создаёт для него отдельный метод.

execute и executeAs: в чём разница

В generated-клиенте есть два основных способа выполнить запрос:

execute(...)
.executeAs(...)

execute(...) делает обычный RestAssured-вызов и отдаёт управление наружу. Он удобен, когда у ответа нет тела.

cohortsDefaultApi()
        .addUsers()
        .idPath(cohortId)
        .body(request)
        .execute(Validatable::then)
        .statusCode(204);

executeAs(...) используется, когда в Swagger описана response-схема. Под капотом он выполняет запрос и десериализует ответ в generated-модель:

CreateCohortsRs result = cohortsDefaultApi()
        .createCohort()
        .body(request)
        .executeAs(response -> response
                .then()
                .statusCode(201)
                .extract().response());

Для нас это не просто удобная обёртка. Это дополнительная контрактная проверка.

Если Swagger говорит, что ответ должен быть CreateCohortsRs, то тест пытается прочитать фактический JSON именно как CreateCohortsRs. Если backend вернёт структуру, которая не соответствует контракту, тест упадёт на десериализации или на проверках.

в Swagger поле называется id, а API вернул cohort_id
в Swagger id описан как integer, а API вернул string
в Swagger описан object, а API вернул array
при строгой десериализации backend добавил новое поле, а 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, форкнул проект, поправил проблемы и адаптировал инструмент под наши задачи.

Форк доступен здесь:

https://github.com/fenixnow/swagger-coverage

В README форка подробнее описаны настройки и запуск через Docker:

https://github.com/fenixnow/swagger-coverage#использование-docker

Во время тестов: SwaggerCoverage собирает вызовы

Во время выполнения тестов RestAssured request specification содержит SwaggerCoverageV3RestAssured:

.addFilter(new SwaggerCoverageV3RestAssured(
        new FileSystemOutputWriter(Paths.get("build/swagger-coverage-output/cohorts"))))

Это RestAssured filter. Он встраивается в цепочку выполнения HTTP-запроса и видит каждый запрос, который проходит через наш RequestSpecification.

тест вызывает generated API method
        ↓
generated client выполняет RestAssured request
        ↓
RestAssured пропускает request/response через filters
        ↓
SwaggerCoverageV3RestAssured фиксирует факт вызова
        ↓
данные сохраняются в build/swagger-coverage-output/cohorts

Важно: SwaggerCoverageV3RestAssured не строит HTML-отчёт прямо во время теста. Он только собирает сырые данные: какой endpoint был вызван, каким HTTP-методом и какой статус вернул сервер.

GitLab CI: тесты и сбор данных для coverage

В GitLab CI этот процесс разбит на два последовательных шага: сначала прогоняются API-тесты, затем на основе результатов тестового прогона автоматически собираются отчёты о покрытии.

Во время тестового прогона Gradle запускает нужные test tasks. Перед компиляцией тестов он скачивает Swagger, генерирует Java-клиенты и модели, а затем запускает JUnit/RestAssured-тесты.

Во время выполнения тестов SwaggerCoverage сохраняет фактические HTTP-вызовы в директорию:

build/swagger-coverage-output

После завершения тестов GitLab сохраняет эту директорию как artifact вместе со Swagger-файлами. Следующая job берёт эти данные и сравнивает:

Swagger/OpenAPI specification и 

фактически выполненные HTTP-вызовы

Именно из этой пары потом строится coverage report.

GitLab CI: jobs для Swagger Coverage

После тестов в пайплайне запускаются отдельные coverage jobs в Docker-образе с доработанным swagger-coverage:

swagger-coverage-report-<env>-banners
swagger-coverage-report-<env>-gifts
swagger-coverage-report-<env>-cohorts

В CI это выглядит как отдельный image для coverage job. Образ содержит swagger-coverage-commandline, поэтому job остаётся простой: ей нужно только передать спецификацию, input-директорию и конфиг.

Для Cohorts команда выглядит так:

swagger-coverage-commandline \
  -s src/test/resources/cohorts.swagger.yaml \
  -i build/swagger-coverage-output/cohorts \
  -c swagger-coverage-cohorts-config.json

Здесь:

-s → Swagger/OpenAPI specification
-i → данные, собранные SwaggerCoverageV3RestAssured во время тестов
-c → конфиг правил отчёта

После этого job копирует результаты в public/cohorts:

cp cohorts-coverage-report.html public/cohorts/cohorts-coverage-report.html
cp cohorts-coverage-results.json public/cohorts/cohorts-coverage-results.json

На выходе получаем два артефакта:

cohorts-coverage-report.html   → красивый HTML для человека
cohorts-coverage-results.json  → структурированный JSON для LLM

HTML отчет выглядит следующим образом:

Как ссылки попадают в Merge Request

Coverage job сохраняет ссылки на HTML и JSON в dotenv artifact:

SWAGGER_COVERAGE_COHORTS_URL_<ENV>=...
SWAGGER_COVERAGE_COHORTS_LLM_URL_<ENV>=...

После этого отдельная job publish-coverage-report публикует комментарий в Merge Request.

Она собирает ссылки по сервисам:

Banners → HTML + LLM JSON
Gifts   → HTML + LLM JSON
Cohorts → HTML + LLM JSON

И пишет в MR сообщение со ссылками на два типа отчётов:

HTML — человекочитаемый отчёт. Его удобно открыть в браузере: посмотреть сводку, методы, группы, варианты покрытия и быстро понять, где есть пробелы.

LLM JSON — структурированный отчёт для модели. HTML красивый, но плохо подходит для автоматического анализа: его неудобно скармливать LLM и сложно стабильно разбирать. Поэтому в доработанной версии swagger-coverage мы добавили отдельный JSON-формат выгрузки результатов покрытия именно для LLM.

В MR это выглядит так:

Вся CI-цепочка целиком

В CI мы используем Docker-образ с доработанной версией swagger-coverage. Его можно запустить как отдельный шаг пайплайна: на вход передаём Swagger/OpenAPI-файл, директорию с результатами, которые собрал SwaggerCoverage, и конфиг отчёта.

В нашем случае CI-цепочка выглядит так:

GitLab CI

        ↓

test job запускает Gradle test task

        ↓

Gradle скачивает Swagger

        ↓

OpenAPI Generator генерирует Java clients/models

        ↓

JUnit/RestAssured тесты выполняются через generated client

        ↓

SwaggerCoverageV3RestAssured пишет build/swagger-coverage-output

        ↓

GitLab сохраняет swagger files и coverage output как artifacts

        ↓

coverage job в Docker-образе запускает swagger-coverage-commandline

        ↓

генерируются HTML и JSON coverage reports

        ↓

publish-coverage-report публикует ссылки в Merge Request

        ↓

LLM получает JSON report и понимает, какие тесты ещё нужно написать

Именно эта связка делает процесс автоматическим. 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 получает на вход два файла:

cohorts.swagger.yaml

cohorts-coverage-results.json

Swagger-файл нужен, чтобы понять контракт API: endpoints, operationId, параметры, request body, response models и статусы.

Coverage JSON — это LLM-friendly отчёт, который формируется в CI после тестового прогона. Его задача — не заменить HTML-отчёт для человека, а дать модели структурированные данные: что покрыто, что не покрыто и какие requirements ещё требуют тестов.

Упрощённо он содержит:

{
  "summary": {
    "total_operations": 89,
    "fully_covered": 5,
    "partially_covered": 20,
    "not_covered": 128,
    "coverage_percent": 13.0
  },
  "paths": {
    "/api/v1/cohort/{id}/users": {
      "GET": {
        "state": "PARTY",
        "coverage": "3/6",
        "deprecated": false,
        "requirements": {
          "status_codes": [],
          "parameters": [],
          "body": [],
          "properties": []
        }
      }
    }
  }
}

Skill анализирует этот JSON и понимает, какие операции находятся в каком состоянии:

EMPTY  → endpoint вообще не покрыт тестами
PARTY  → endpoint покрыт частично
FULL   → endpoint полностью покрыт

Дальше он смотрит на requirements и определяет, что именно не покрыто:

status_codes  → не проверен нужный HTTP-статус
parameters    → не использован path/query/header параметр
body          → не покрыто тело запроса
properties    → не проверены поля ответа

На основе этого skill принимает решение, что делать дальше:

  • EMPTY endpoint → предложить базовый happy-path тест

  • PARTY endpoint → добавить недостающие проверки или сценарии

  • FULL endpoint → пропустить

  • deprecated endpoint → пропустить

После анализа первый skill вызывает второй skill и передаёт ему уже не весь отчёт, а конкретную задачу: какой endpoint покрыть, какой operationId использовать, какие request/response schemas посмотреть и какие requirements закрыть.

Второй skill: генерация тестов по правилам проекта

Второй skill уже не занимается анализом всего coverage report. Он получает от первого skill конкретную задачу: какую операцию нужно покрыть и какие requirements нужно закрыть.

Дальше он работает как исполнитель: смотрит Swagger/OpenAPI-спецификацию, находит нужный generated API method и generated models, проверяет существующие тесты и только после этого пишет автотест по правилам проекта.

При генерации он следует нашим ограничениям:

использовать generated Swagger client
использовать generated models
не писать raw JSON руками
не создавать свои DTO
не собирать URL вручную
класть тесты в правильный package
использовать executeAs или execute по контракту
писать проверки через getters generated-моделей
оформлять шаги через Allure

Например, если coverage показывает, что не покрыт endpoint из Cohorts, skill идёт в Swagger, находит нужную операцию, смотрит operationId, request/response schema и понимает, какой generated-код надо использовать.

API method              → cohortsDefaultApi().getUsers()
path parameter id       → .idPath(...)
query parameter page    → .pageQuery(...)
query parameter size    → .sizeQuery(...)
response model          → ListUsersWithPagination
execution method        → executeAs(...)

Это важно: LLM не должна придумывать свой способ работы с API. Она должна пользоваться тем же контрактным клиентом, которым пользуются обычные тесты.

Когда одного Swagger недостаточно

Важно сказать честно: не каждый автотест можно сгенерировать только по Swagger и coverage report.

Для простых CRUD-методов этого часто достаточно: есть endpoint, понятный request body, понятный response body, можно построить happy-path тест и базовые проверки.

Но бывают сложные микросервисы, где сам вызов одного метода требует большого количества предусловий:

создать сущность в другой системе
подготовить пользователя с нужным состоянием
получить корректный токен
заранее включить feature flag
передать параметры, которые нельзя угадать из Swagger
соблюсти бизнесовую последовательность вызовов

Swagger хорошо описывает контракт конкретного HTTP-метода, но он не всегда объясняет бизнес-контекст: откуда взять данные, в каком состоянии должна быть система, какие шаги нужно выполнить до вызова endpoint.

В таких случаях нам нужны тест-кейсы, написанные QA. Там уже описаны предусловия, шаги, тестовые данные и ожидаемый результат.

Для этого мы используем отдельные skills, которые работают не от coverage report, а от тест-кейсов в ТестОпс. Мы написали свой MCP для ТестОпс, через который LLM может получить тест-кейс, разобрать его и превратить в автотест.

Allure TestOps test case
        ↓
наш MCP для Allure TestOps
        ↓
LLM skill читает предусловия, шаги и expected result
        ↓
skill сопоставляет шаги с OpenAPI/Swagger
        ↓
находит generated API methods и models
        ↓
пишет автотест по правилам проекта

Так мы не пытаемся заменить 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:

cohorts.api.CohortsApi
cohorts.ApiClient
cohorts.model.CreateCohortsRq
cohorts.model.CreateCohortsRs
cohorts.model.ListUsers
cohorts.model.ListUsersWithPagination

Его не пишем руками. Он появляется после генерации OpenAPI Generator.

Наш тестовый код:

BaseCohortTests
CohortsSpec
CohortV1Tests

Он отвечает за сценарии, шаги, проверки, токены, base URL и подключение фильтров.

Если упростить разделение ответственности, получается так:

Swagger описывает контракт

        ↓

OpenAPI Generator создаёт API-классы и модели

        ↓

Mustache templates задают форму generated-кода

        ↓

BaseCohortTests подключает generated client к тестовой инфраструктуре

        ↓

CohortV1Tests описывает конкретный тестовый сценарий

То есть generated-код отвечает за технический контракт API, а наш тестовый код — за сценарий, данные, шаги и проверки. Всё, что связано с coverage и CI, живёт уровнем выше и уже было разобрано в отдельном разделе.

Что это даёт

Главная польза подхода в том, что автотесты становятся ближе к API-контракту.

Если в Swagger поменяется operationId, изменится имя generated-метода. Если поменяется request body, изменится generated-модель. Если из ответа исчезнет поле, пропадёт getter. Многие расхождения становятся видны уже на этапе компиляции или десериализации, а не через неделю после релиза.

Для QA тест при этом остаётся читаемым:

CreateCohortsRs result = cohortsDefaultApi()
        .createCohort()
        .body(request)
        .executeAs(response -> response
                .then()
                .statusCode(201)
                .extract().response());

По этой цепочке сразу видно:

какой API вызываем
какое тело отправляем
какой статус ожидаем
в какую модель десериализуем ответ

Swagger Coverage закрывает вторую часть задачи: показывает, какие операции из Swagger реально были вызваны тестами, а какие ещё остались непокрытыми.

LLM-skills закрывают третью часть: помогают превратить этот отчёт в новые автотесты, но не хаотично, а по правилам проекта.

А тест-кейсы из ТестОпс закрывают сложные сценарии, где одного Swagger недостаточно и нужен бизнесовый контекст.

В итоге Swagger перестаёт быть просто страницей с документацией. Он становится рабочим контрактом между аналитиками, разработчиками и QA, а coverage report - не просто красивым HTML-отчётом, а источником задач для дальнейшего развития автотестов.

Что дальше

В этой статье мы разобрали только один кусок большого процесса - API-автотесты от контракта и coverage report для LLM.

В следующих материалах расскажем, как мы идём дальше: разбираем упавшие автотесты через дефекты в ТестОпс, связываем это с Яндекс Трекером и используем ИИ не только для генерации тестов, но и для анализа результатов и создания задач.

Отдельно покажем, как строим систему, где ИИ подключается к нашим сервисам, сам готовит тест-планы, проверяет задачу через БД, REST и gRPC, а итоговый отчёт публикует прямо в задачу в Яндекс Трекере.

Так что не переключайтесь - дальше будет ещё интереснее.