Платформы

Spring Boot — @WebMvcTest и @SpringBootTest в одном прогоне, дефект из упавшего контекста

Spring Boot наследует Surefire и Failsafe от spring-boot-starter-parent, и обе группы тестов — быстрые срезы и тяжёлые интеграционные — QA Деск принимает одним прогоном. Ключ кейса ставится в @DisplayName; настройка Surefire одна и та же, что для любого Java-проекта.

Как загрузить тесты Spring Boot в QA Деск

Одной командой после mvn verify: qadesk-cli upload target/surefire-reports target/failsafe-reports. Срезы @WebMvcTest и @DataJpaTest Surefire прогоняет на фазе test, классы *IT с @SpringBootTest Failsafe — на integration-test, и оба каталога уезжают одним прогоном с одним ключом идемпотентности.

# GitLab CI; QADESK_TOKEN — в CI/CD Variables, masked
verify:
  stage: test
  when: always
  services: [docker:dind]          # для Testcontainers
  script:
    - mvn -B verify -Dmaven.test.failure.ignore=true
    - curl -fsSL https://qadesk.ru/cli/qadesk-cli-linux-amd64 -o qadesk-cli && chmod +x qadesk-cli
    - ./qadesk-cli upload --run "$CI_PROJECT_NAME verify" --env test --platform api
        target/surefire-reports target/failsafe-reports

--platform api ложится в поле прогона: по нему потом фильтруется история бэкенда отдельно от e2e фронта. Стенд, на котором это проверено 22.09.2026: Spring Boot 3.4.1, spring-boot-starter-test, OpenJDK 21, Maven 3.9.9.

Как пометить тест Spring Boot ключом кейса

В @DisplayName: @DisplayName("QAD-C40 /health отвечает 200"). Ключ кейса содержит дефис, а имя Java-метода дефис не допускает, поэтому другого места для него нет. Чтобы Surefire записал @DisplayName в отчёт, у обоих плагинов включается usePhrasedTestCaseMethodName — родитель Spring Boot версии плагинов задаёт сам, указывать их не нужно.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <statelessTestsetReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
      <usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
    </statelessTestsetReporter>
  </configuration>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <configuration>
    <statelessTestsetReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
      <usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
    </statelessTestsetReporter>
  </configuration>
  <executions><execution><goals><goal>integration-test</goal><goal>verify</goal></goals></execution></executions>
</plugin>
@WebMvcTest(HealthController.class)
class HealthWebTest {
    @Autowired MockMvc mvc;

    @Test @DisplayName("QAD-C40 /health отвечает 200 и ok")
    void health() throws Exception {
        mvc.perform(get("/health")).andExpect(status().isOk()).andExpect(content().string("ok"));
    }
}

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class AppIT {
    @Test @DisplayName("QAD-C42 контекст поднимается и /health живой")
    void context() { … }
}

В отчёте появляются <testcase name="QAD-C40 /health отвечает 200 и ok" classname="demo.HealthWebTest"> и <testcase name="QAD-C42 …" classname="demo.AppIT">, приёмник кладёт их в строки кейсов QAD-C40 и QAD-C42. Без usePhrasedTestCaseMethodName в отчёт уходят имена методов health и context, и оба результата возвращаются как несматченные.

Что делать с упавшим контекстом Spring

Упавший контекст — одна красная строка в прогоне с текстом ошибки, а не сорок красных тестов. Когда @SpringBootTest не поднялся, Surefire пишет <error> в каждый тест класса, приёмник считает <error> падением, а повторяющийся ключ в одном отчёте берёт худший исход.

Сообщения при этом разные: первому тесту достаётся Failed to load ApplicationContext с причиной, остальным — ApplicationContext failure threshold (1) exceeded, потому что Spring 6.1+ не поднимает упавший контекст повторно. Проверено на стенде 22.09.2026.

Дефект заводится из этой строки: текст ошибки переезжает в наблюдение, сборка и ветка заполняются из полей прогона, а в карточке прогона остаётся ссылка на задание CI. Разработчик открывает дефект и видит стек, а не «интеграционные упали, посмотри пайплайн».

Как быть с Testcontainers и тяжёлыми тестами

Тяжёлые тесты живут в Failsafe и не мешают срезам. Стандартное разделение Spring Boot — @WebMvcTest, @DataJpaTest, @JsonTest в Surefire за секунды; @SpringBootTest с Testcontainers (PostgreSQL, Kafka, Redis) в Failsafe — минуты. QA Деск это разделение не ломает: два каталога, один прогон, и в покрытии видно, какой кейс закрыт срезом, а какой — интеграционным тестом с настоящей базой.

Если интеграционные тесты запускаются отдельным заданием и позже, они загружаются отдельно с собственным --ci-run-id или --attempt. Под общим ключом второй, другой отчёт получит отказ 409 — приёмник не сливает результаты молча и не перезаписывает первый прогон вторым.

Чего в продукте нет

  • нет стартера или автоконфигурации qadesk-spring-boot-starter: разметка — стандартный @DisplayName, выгрузка — бинарник без зависимостей в pom.xml;
  • ключ через @Tag, @Sql или TestReporter не читается — в отчёт Surefire эти данные не попадают;
  • QA Деск не запускает тесты и не собирает JaCoCo — покрытие кода остаётся в CI;
  • нет сопоставления application.yml-профилей с окружениями прогона: --env передаётся явно.

Частые вопросы

Нужно ли указывать версии Surefire и Failsafe?

Нет, если проект наследует spring-boot-starter-parent: он задаёт версии обоих плагинов. Достаточно секции statelessTestsetReporter с usePhrasedTestCaseMethodName в конфигурации каждого. У проекта на spring-boot-dependencies без родителя версия указывается своя, стенд проверен с 3.5.2.

Можно ли передать ключ кейса через @Tag?

Нет. @Tag фильтрует запуск, но в JUnit XML Surefire не попадает, и приёмник его не видит. Ключ кейса QA Деск читается из имени теста, а в имя Surefire записывает @DisplayName — при включённом usePhrasedTestCaseMethodName.

Как загрузить, если интеграционные тесты идут отдельным заданием?

С собственным ключом: --ci-run-id "$CI_PIPELINE_ID-it" или своим --attempt. Два разных отчёта под одним ключом — отказ 409 на второй, это защита от потерянных шардов, а не ошибка настройки.

Что попадает в дефект из упавшего @SpringBootTest?

Сообщение из <error> или <failure> целиком — со стеком, каким его записал Surefire, — плюс окружение, сборка, ветка и ссылка на задание CI из полей прогона. Заводит дефект человек кнопкой из строки прогона; автоматически дефекты не создаются.

Работает ли это с Kotlin и Gradle?

Да. Gradle пишет @DisplayName в отчёт без настройки, а имя Kotlin-функции в обратных кавычках может содержать дефис и ключ напрямую. Подробности и пример с Kotest — на странице Kotlin.

С чего начать

  1. Заведите проект и выпустите токен приёма: Настройки → Интеграции → CI / автотесты.
  2. Добавьте usePhrasedTestCaseMethodName в Surefire и Failsafe и шаг qadesk-cli upload с двумя каталогами после mvn verify.
  3. Возьмите кейсы регистрации и авторизации и проставьте их ключи в @DisplayName тестов контроллеров и security-конфигурации.
  4. Заведите тест-план на релиз с миграциями Flyway или Liquibase на копии боевых данных — это то, что mvn verify не проверяет.

Попробуйте на своём проекте

Заведите проект, загрузите первый отчёт или заведите первый дефект — бесплатный тариф без карты. Нужно демо на ваших сценариях — напишите на hello@qadesk.ru.

Создать проект

Рядом по теме

Платформы

Java · Maven / Gradle

Maven и Gradle уже пишут JUnit XML — QA Деск принимает его без плагинов. Единственная тонкость: ключ кейса `QAD-C31` содержит дефис, а имя Java-метода дефис содержать не может, поэтому ключ ставится в `@DisplayName`, и Surefire надо попросить его записать.

Java — отчёты Surefire и Gradle в покрытие, ключ кейса в @DisplayName
Платформы

Kotlin

В Kotlin имя тестовой функции пишется в обратных кавычках и может содержать дефис — ключ кейса `QAD-C31` ставится прямо в имя, а Gradle записывает его в отчёт без единой настройки. Kotest даёт то же самое строкой в `StringSpec`.

Kotlin — ключ кейса прямо в имени теста, отчёт Gradle без настройки
Автотесты, CI и мониторинг

Sentry

Ошибка уровня error или fatal становится дефектом выбранного проекта — с типом исключения, местом и ссылкой на issue. Одна ошибка — один дефект, повторные срабатывания дублей не плодят.

Sentry — ошибка у пользователя приходит дефектом