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

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

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

В README форка подробнее описаны настройки и запуск через 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, а итоговый отчёт публикует прямо в задачу в Яндекс Трекере.

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