JavaRush /Курсы /Spring Boot /Тесты health и CatalogData...

Тесты health и CatalogDataHealthIndicator

Spring Boot
27 уровень , 3 лекция
Открыта

1. Роль health рядом с web smoke

Когда вы впервые слышите «давайте тестировать /actuator/health», это легко воспринять как «ещё один endpoint, ну и ладно». Но health — это не обычная ручка API и не «ещё один JSON». Это формальный сигнал о состоянии сервиса: что он не просто запустился, а готов быть полезным. Если web smoke отвечает на вопрос «сервер вообще отвечает?», то health отвечает «он отвечает в здравом уме?».

В catalog-service это особенно заметно, потому что наш сервис read-only и живёт на данных из конфигурации. Теоретически приложение может запуститься, контроллеры могут быть доступны, но по факту сервис может быть «пустым» или «сомнительным»: например, каталог загрузился без опубликованных курсов, или есть противоречивые данные. Пользователю от такого «живого, но бесполезного» сервиса не легче.

Именно поэтому мы в проекте делали CatalogDataHealthIndicator: он проверяет не фреймворк и не сеть, а доменно понятные условия. В нашей формулировке сервис «здоров», когда каталог загрузился, есть хотя бы один опубликованный курс, и нет дублирующихся slug. Это похоже на проверку «в машине есть двигатель, бензин и колёса не отваливаются» — не гарантия, что вы доедете в отпуск, но без этого точно никуда.

Важно ещё и то, что health — это та вещь, которая в реальности часто используется автоматикой: мониторингом, балансировщиком, оркестраторами. Мы не уходим в Kubernetes и production hardening (это отдельные дисциплины), но мыслить «health как контракт» полезно уже сейчас — чтобы сервис был не только учебным, но и инженерно вменяемым.

2. Модель: HealthIndicatorHealth → endpoint

Чтобы тестировать health уверенно, нужно не гадать, а понимать цепочку: кто что вызывает и где появляется JSON. В Spring Boot Actuator есть слой индикаторов, которые возвращают объект Health (внутри статус и детали), и есть слой endpoint'а, который собирает результаты разных индикаторов и отдаёт наружу итоговый ответ. Если держать эту схему в голове, вы легко поймёте, что именно вы тестируете в каждом тесте.

Упрощённо это можно представить так:

flowchart TD
    A[GET /actuator/health] --> B[Actuator Health Endpoint]
    B --> C[DiskSpaceHealthIndicator]
    B --> D[CatalogDataHealthIndicator]
    C --> E[Health: UP/DOWN + details]
    D --> F[Health: UP/DOWN + details]
    E --> G[Aggregated Health]
    F --> G
    G --> H[JSON response]

Здесь есть три «уровня реальности»:

1) Наш индикатор (CatalogDataHealthIndicator) — это чистая логика, которую мы написали сами. Он возвращает Health, обычно через builder Health.up() или Health.down().

2) Агрегатор Actuator — это фреймворк-часть, которая собирает вклад разных индикаторов и делает общий статус. Мы не обязаны тестировать «правильность агрегатора» — это задача Spring Boot, он и так этим занимается много лет.

3) Web-представление /actuator/health — это то, что видит внешний мир. Здесь вступают в игру настройки экспозиции endpoint’ов, base path, а также настройки вроде «показывать ли details».

Полезно зафиксировать маленькую таблицу, чтобы не путать слои:

Что проверяем Где это живёт Что означает падение
indicator.health() возвращает UP в нашем коде сломана доменная логика health или зависимость индикатора
GET /actuator/health отвечает и содержит UP в web-слое Actuator endpoint не поднялся/не экспонирован/сломалась интеграция или общий health стал не UP

Отдельный маленький нюанс, который часто удивляет новичков: Status в Actuator — это не «какая-то строка», а объект (например, Status.UP). А вот JSON в endpoint’е обычно содержит строковое значение "UP". Поэтому в коде вы сравниваете Status.UP, а в JSON-проверке — строку "UP".

3. Тест индикатора напрямую

Самая чистая часть health-тестирования — проверять индикатор напрямую, минуя HTTP, MockMvc и формат JSON. Это почти идеальный «инженерный компромисс»: тест остаётся Boot-centric (мы поднимаем контекст, получаем реальный бин), но проверяем то, что принадлежит нам — результат health().

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

Файл: src/test/java/com/example/catalogservice/actuator/CatalogDataHealthIndicatorTest.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest // Поднимаем Spring-контекст, чтобы получить реальные бины
class CatalogDataHealthIndicatorTest {
    @Autowired
    CatalogDataHealthIndicator indicator; // Тестируем именно наш HealthIndicator
}

Теперь добавим самый простой позитивный сценарий: при нормальной конфигурации проекта индикатор должен говорить UP. Это тест не о том, сколько именно курсов у вас в конфиге, не о том, в каком порядке они лежат и не о том, какие поля сериализуются в JSON. Это тест о базовой гарантии: «каталог в порядке».

Файл: CatalogDataHealthIndicatorTest.java (фрагмент)

import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Status;

import static org.junit.jupiter.api.Assertions.assertEquals;

@Test
void reportsUpForValidCatalog() {
    // Проверяем доменный итог: сервис "готов", значит статус должен быть UP
    assertEquals(Status.UP, indicator.health().getStatus());
}

Обратите внимание на стиль assertions: мы сравниваем Status.UP, а не "UP". Это важная мелочь: вы тестируете Java-логику и контракт API Actuator, а не JSON-представление.

Про Health: что стоит проверять

Когда вы вызываете indicator.health(), вы получаете Health. У него есть getStatus() и getDetails(). В details мы часто кладём полезные доменные подсказки: сколько опубликованных курсов, есть ли дубли, загружен ли каталог. Это удобно для диагностики, но тут есть ловушка: если вы начнёте проверять все details до последнего ключа, тест станет хрупким.

Хороший подход для начинающего — либо ограничиться статусом, либо проверять один-два стабильных факта. Например: «в details есть ключ publishedCourses и значение больше нуля». Это не привязывает тест к точному числу курсов и не заставляет вас править тест каждый раз, когда вы добавили ещё один курс в конфиг.

Файл: CatalogDataHealthIndicatorTest.java (фрагмент)

import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;

import static org.junit.jupiter.api.Assertions.assertTrue;

@Test
void includesDetailsWeOwn() {
    Health health = indicator.health(); // Вызываем индикатор напрямую, без HTTP-слоя

    // Детали — это диагностическая информация, которую мы "владеем" в рамках индикатора
    assertTrue(health.getDetails().containsKey("publishedCourses")); // ключ есть
}

Если у вас в CatalogDataHealthIndicator нет такого detail-ключа — ничего страшного, этот фрагмент просто показывает идею. Смысл в том, что проверять стоит те «детали», которые вы считаете частью контракта вашего индикатора, а не все внутренности подряд.

Почему же полезно иметь и тест индикатора напрямую, и endpoint-смоук? Потому что если тест на indicator.health() упадёт, вы мгновенно понимаете область проблемы: это ваша логика health, или зависимость, на которую она опирается. Вам не нужно гадать «это MockMvc? это сериализация? это base-path? это exposure policy?». Вы смотрите на индикатор, и точка.

Это похоже на диагностику машины: если вы проверили аккумулятор отдельно и он мёртв, вам не нужно проверять радио, фары и дворники. Да, всё не работает — но причина локализована.

4. Smoke-тест /actuator/health через MockMvc

Проверить индикатор напрямую — отлично, но этого недостаточно, чтобы быть уверенным в «операционной поверхности» сервиса. Можно случайно отключить Actuator, можно закрыть endpoint настройками exposure, можно поменять base path или порт управления. В итоге индикатор может быть идеальным, но внешний мир его никогда не увидит. Поэтому мы добавляем второй тест: лёгкий endpoint smoke.

Начнём с подготовки теста с MockMvc. Здесь всё знакомо по предыдущей лекции: @SpringBootTest + @AutoConfigureMockMvc, и мы получаем объект MockMvc, чтобы вызвать endpoint без реальной сети.

Файл: src/test/java/com/example/catalogservice/actuator/HealthEndpointSmokeTest.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest // Поднимаем контекст приложения
@AutoConfigureMockMvc // Включаем MockMvc, чтобы дергать endpoint'ы без реального HTTP-сервера
class HealthEndpointSmokeTest {
    @Autowired
    MockMvc mockMvc; // Через него делаем GET /actuator/health
}

Теперь сам тест. Здесь важно держать себя в руках (в хорошем смысле): мы не делаем «полный снимок JSON» и не сравниваем его целиком. Для smoke достаточно проверить, что endpoint доступен и что общий статус "UP".

Файл: HealthEndpointSmokeTest.java (фрагмент)

import org.junit.jupiter.api.Test;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void exposesHealthEndpoint() throws Exception {
    mockMvc.perform(get("/actuator/health")) // Дергаем actuator endpoint так же, как это сделает внешний мир
            .andExpect(status().isOk()) // В здоровом состоянии обычно 200 OK
            .andExpect(jsonPath("$.status").value("UP")); // Здесь проверяем именно JSON-представление, поэтому строка
}

Здесь мы сравниваем строку "UP", потому что тестируем JSON-представление endpoint’а. И это нормально: индикатор тестируется через Status.UP, endpoint — через "UP".

jsonPath("$.status") вместо contains("UP")

В прошлых примерах часто делают body.contains("UP"), и для совсем первого знакомства это окей. Но как только в ответе появятся дополнительные поля или вы добавите health components, строковый поиск может стать менее надёжным. jsonPath("$.status") говорит прямо: «меня интересует конкретное поле status», и тест остаётся читаемым.

При этом мы всё равно не делаем тест хрупким: мы проверяем только один ключевой сигнал, а не всю структуру.

Нюанс про HTTP-статус и 200 OK

В здоровом состоянии сервиса общий health обычно UP, и Actuator отвечает 200. Если бы общий health стал DOWN, то Actuator может вернуть 503 Service Unavailable (и это, кстати, разумно: «я жив, но я не готов»). Мы не строим сейчас сложную матрицу негативных сценариев, но важно помнить: не всегда health = 200. Зависит от общего статуса и настроек.

5. Границы health-тестов

Любой тестовый набор умирает не от недостатка тестов, а от их «неуправляемого расползания». Health-тесты особенно склонны к этому: вы открываете /actuator/health, видите красивый JSON, и рука тянется проверить всё: disk space, uptime, компоненты, детали… И вот вы уже тестируете то, что не вы писали, и то, что может меняться от окружения к окружению.

Самый здоровый (простите за каламбур) подход — помнить границу ответственности. Наши тесты должны отвечать на два конкретных вопроса. Первый вопрос: «Наш CatalogDataHealthIndicator действительно корректно определяет состояние каталога?». Второй вопрос: «Endpoint /actuator/health действительно доступен и отдаёт общий статус UP?». Всё, что глубже, либо относится к отдельному курсу по тестированию, либо превращает тесты в тяжёлые и хрупкие.

Чтобы эта граница была совсем осязаемой, держите в голове простую расшифровку падений:

Упал тест Что это значит на практике Самое вероятное место проблемы
CatalogDataHealthIndicatorTest сломали правило здоровья каталога или его зависимость код индикатора, сервис/репозиторий, конфигурация данных
HealthEndpointSmokeTest endpoint недоступен или общий health не UP exposure/base-path/actuator wiring или реальная проблема со здоровьем

Ещё один тонкий момент: не пытайтесь делать health-тест «про бизнес-логику каталога». Если вы в health-тесте начинаете проверять фильтрацию курсов, обработку query params и прочие вещи — вы смешиваете уровни. Health не должен быть вторым API для каталога, и тест health не должен превращаться в альтернативный тест контроллера.

И наконец, если вам очень хочется протестировать негативный сценарий health (например, «нет опубликованных курсов ⇒ DOWN»), делайте это максимально локально и понятно. В идеале вы создаёте в тесте контролируемые условия (например, подсовываете индикатору тестовые данные через зависимость) и проверяете только статус. Но на уровне этого курса мы держимся минимального набора: один позитивный тест на индикатор и один smoke на endpoint — уже отличный базовый слой уверенности.

6. Типичные ошибки при health-тестах

Ошибка №1: проверять только /actuator/health и игнорировать сам CatalogDataHealthIndicator.
Если вы тестируете только endpoint, вы не различаете две ситуации: «ваша логика health сломана» и «endpoint не экспонирован/не подключен». В результате при падении теста вы получаете более размытый сигнал и начинаете искать проблему по всему проекту. Прямая проверка indicator.health() делает причину падения гораздо более локальной и понятной.

Ошибка №2: сравнивать весь JSON ответа /actuator/health целиком.
Полный JSON легко меняется: могут добавиться компоненты, поменяться детали, появиться новые индикаторы от Boot, или измениться конфигурация show-details. Если вы сравниваете весь ответ строкой, тест превращается в «сторожевого пса» против любых изменений, даже полезных. Гораздо устойчивее проверять только $.status и, максимум, один-два стабильных элемента, которые вы действительно считаете контрактом.

Ошибка №3: пытаться тестировать через health бизнес-логику каталога.
Health — это сигнал «готов/не готов», а не «список курсов корректно фильтруется по track». Как только в health-тестах появляются проверки фильтрации, сортировки или формы ответа каталога, вы смешиваете responsibilities. Это почти всегда приводит к тому, что тесты становятся перегруженными и начинают падать «не по адресу».

Ошибка №4: ожидать, что health всегда возвращает 200 OK.
В состоянии UP — да, обычно 200. Но как только общий статус становится DOWN, Actuator может отвечать 503. Если вы когда-нибудь будете добавлять негативные сценарии, не делайте проверку «всегда должен быть 200». Проверяйте то, что вы действительно хотите: статус в JSON или ожидаемый HTTP-статус для конкретного состояния.

Ошибка №5: проверять детали health endpoint'а, не контролируя настройки show-details.
Даже если ваш индикатор кладёт полезные детали в Health, endpoint может их не показать (и часто так и нужно, особенно вне local/dev). Поэтому проверка деталей через /actuator/health может быть нестабильной и зависящей от профиля. Если вам очень нужны детали — проверяйте их напрямую через indicator.health().getDetails() и считайте это контрактом вашего индикатора, а не web-ответа.

1
Задача
Spring Boot, 27 уровень, 3 лекция
Недоступна
Прямой тест custom `HealthIndicator`
Прямой тест custom `HealthIndicator`
1
Задача
Spring Boot, 27 уровень, 3 лекция
Недоступна
Smoke-тест `/actuator/health`
Smoke-тест `/actuator/health`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ