JavaRush /Курсы /Spring Test /Единый стиль проверок в тестах

Единый стиль проверок в тестах

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

1. Важность единого стиля assertions

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

Представьте набор тестов ContentHub через пару недель обучения. Там будут unit-тесты бизнес-правил (например, переходов статусов статьи), тесты JSON-контракта DTO, потом тесты HTTP-границы контроллеров, дальше — слой данных и интеграционные сценарии. Если в каждом слое у вас «свой язык», мозг будет тратить время не на смысл, а на расшифровку синтаксиса: здесь Hamcrest, там AssertJ, тут JSON сравниваем как строку, а вон там вообще парсим руками. Это как читать одну книгу, где каждая глава написана разным алфавитом: технически можно, но удовольствие сомнительное.

Инструментов у нас уже несколько, и теперь главный риск не в отсутствии очередного метода, а в том, что набор тестов начинает говорить сразу на нескольких диалектах.

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

2. Выбираем инструмент не по вкусу, а по форме результата

Очень хочется выбирать библиотеку по принципу «мне так красивее». Но в тестах это ловушка: красота быстро превращается в хаос, если у каждого разработчика вкус свой. Гораздо устойчивее выбирать инструмент так же, как мы выбираем тип теста: от задачи и формы результата, а не от личных предпочтений. Вопрос «какой инструмент проверок брать» начинается не с названия библиотеки, а с вопроса: что именно я получил на выходе теста?

Если тест вернул простое значение или объект Java, наша «родная среда» — AssertJ. Если на выходе JSON-строка (что будет часто на границе API), мы либо сравниваем JSON как контракт целиком (JSONassert), либо читаем конкретные поля (JsonPath) и уже эти значения проверяем AssertJ. Если инструмент или фреймворк вокруг вас ожидает matcher-объекты (а такое встречается в экосистеме Spring), Hamcrest становится неизбежным, но мы стараемся держать его на периферии, чтобы он не стал главным «диалектом» тестов.

Чтобы было совсем наглядно, вот маленькое «дерево решений» — не догма, а быстрый ориентир:

flowchart TD
    A["Что вы проверяете?"] --> B["Java-значение / объект"]
    A --> C["JSON как String"]

    B --> BJ["AssertJ: assertThat(actual)..."]

    C --> D["Нужна проверка целого JSON-контракта?"]
    D -->|Да| E["JSONassert: структурное сравнение"]
    D -->|Нет, нужен фрагмент| F["JsonPath: достать поле/элемент"]
    F --> BJ

    A --> G["Инструмент требует matcher?"]
    G -->|Да| H["Hamcrest точечно (MatcherAssert.assertThat)"]
    G -->|Нет| BJ

Важно заметить, что это дерево не говорит «Hamcrest плохой» или «JSONassert всегда лучше». Оно говорит другое: форма результата диктует инструмент, а не наоборот. И это сильно экономит время, когда вы пишете или читаете тест под давлением дедлайна (а дедлайн, как известно, всегда где-то рядом и любит подкрадываться бесшумно).

3. AssertJ как «язык по умолчанию» для тестов ContentHub

С Java-результатами решение уже почти не вызывает споров: строка, число, enum, коллекция, Optional, обычный доменный объект — по умолчанию берём AssertJ. Причина не в том, что он «самый модный», а в том, что он удерживает один и тот же ритм чтения теста: начинаем с actual и формулируем ожидания цепочкой, а не разбрасываем набор разрозненных asserts.

Здесь важен не повтор каталога методов, а сама привычка. Если результат живёт в мире Java-объектов, первым выбором почти всегда будет AssertJ. Исключения бывают, но их лучше воспринимать именно как исключения, а не как повод каждый раз переключать диалект.

JSON: JSONassert, JsonPath и AssertJ

На API-границе меняется не логика проверки, а форма результата: вместо объекта или коллекции часто приходит JSON-строка. И здесь уже бессмысленно спорить со строкой посимвольно. Если нужно зафиксировать payload как контракт целиком, берём JSONassert. Если нужно доказать отдельное правило внутри payload, берём JsonPath, достаём нужное поле и формулируем ожидание тем же AssertJ.

Хороший тест обычно выбирает один из этих режимов, а не оба сразу. Либо мы сравниваем форму JSON как единый контракт, либо адресно проверяем конкретные поля и связи. Так тест остаётся и точным, и не слишком хрупким.

Hamcrest: как не путать с AssertJ

Hamcrest полезно держать рядом не как второй основной язык, а как matcher-диалект для мест, где API просит именно matcher. В обычной Java-проверке он редко даёт выигрыш по читаемости относительно AssertJ, зато легко добавляет путаницу с двумя разными assertThat.

Поэтому рабочее правило простое: если мысль нормально выражается через AssertJ, так и делаем. Если внешний API ждёт matcher, подключаем Hamcrest точечно и лучше явно через MatcherAssert.assertThat(...). Когда эта граница понятна, общая карта по слоям тестов собирается почти без споров.

4. Матрица стилей: unit/JSON/MVC/data/live-server

Даже до появления MVC-, data- и live-server-сценариев полезно заранее договориться, каким языком будут описываться их проверки. Иначе один слой начнёт жить на fluent-цепочках, другой — на matcher-диалекте, а JSON-граница — на случайных строковых сравнениях. Это как договориться о языке общения в команде до того, как вы начали строить общий дом. Поменять потом можно, но будет больно, потому что часть стен уже из бетона, а часть — из разговоров в чате.

Ниже — практическая матрица выбора. Это не список «так надо всегда», а полезная карта: какие типы результатов чаще всего встречаются на каждом уровне тестов ContentHub и каким инструментом их удобнее проверять, сохраняя единый стиль.

Уровень теста в ContentHub Что обычно на выходе теста Доминирующий инструмент Дополнительный инструмент Почему так проще читать и поддерживать
Unit (plain Java) String, числа, enum, List, доменные объекты AssertJ иногда JsonPath/JSONassert (редко) В unit-слое мы чаще всего проверяем Java-объекты и бизнес-правила, и AssertJ здесь самый прямой язык
JSON-контракт (DTO) JSON как строка JSONassert JsonPath (для отдельных правил), AssertJ (для значений) Контракт важно сравнивать структурно, а не посимвольно; точечные правила удобно вытаскивать JsonPath
MVC (mocked HTTP) статус/заголовки + JSON body AssertJ Hamcrest там, где инструмент требует matcher; JSONassert/JsonPath для body Даже если где-то встретится matcher-API, смысловые проверки тела ответа лучше держать в нашем базовом стиле
Data (репозитории/БД) коллекции сущностей, поля, Optional AssertJ иногда сравнение строк/дат аккуратно Слой данных — это много коллекций, объектов и вложенных данных; AssertJ отлично покрывает это без ручных циклов
Live-server (реальный HTTP) статус/заголовки + JSON body как строка AssertJ JSONassert/JsonPath для body Здесь вы почти всегда получаете JSON как строку, но проверяете смысл, поэтому JSON-инструменты работают как «адаптер»

Общий вывод, который нам важнее таблицы: AssertJ — общий язык, а JSONassert/JsonPath/Hamcrest — специализированные инструменты, которые мы подключаем, когда форма результата этого требует.

Чтобы это не осталось теорией, возьмём типичный ответ в виде JSON-строки и проверим одно конкретное правило. Spring здесь не нужен: нам важен именно стиль, а не способ получения строки.

import com.jayway.jsonpath.JsonPath;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ResponseAssertionsStyleTest {

    @Test
    void response_should_contain_error_code() {
        String body = """
                {"status":404,"errorCode":"ARTICLE_NOT_FOUND"}
                """;

        // Извлекаем только то поле, которое важно для этого сценария.
        String errorCode = JsonPath.read(body, "$.errorCode");

        // Проверку формулируем в основном стиле suite — через AssertJ.
        assertThat(errorCode).isEqualTo("ARTICLE_NOT_FOUND");
    }
}

Здесь стиль однородный: итоговое ожидание в AssertJ, а JsonPath — просто способ вытащить фрагмент из JSON. И этот же паттерн будет одинаково уместен и в JSON-контракт тестах, и в MVC тестах, и в live-server тестах — меняется только источник строки body.

7. Минимальные вспомогательные методы для однородного набора тестов

Когда тестов становится много, появляется соблазн написать «свой фреймворк поверх фреймворков». Обычно всё начинается с невинной идеи «давайте сделаем helper для JSON», а заканчивается тем, что никто, кроме автора, не понимает, что реально проверяется. Поэтому здесь нужен очень взрослый компромисс: вспомогательные методы должны сокращать только шум, но не должны прятать смысл теста.

Для нашего курса разумный минимум — это пара функций для повторяющихся операций: прочитать значение по JsonPath и сделать lenient JSON-сравнение. Они не превращают тест в магию, потому что всё равно видно, какой path читаем и что сравниваем.

Вот пример такого «тонкого» помощника:

import com.jayway.jsonpath.JsonPath;
import org.skyscreamer.jsonassert.JSONAssert;

public final class JsonTestSupport {

    private JsonTestSupport() {
        // Утилитарный класс: экземпляры не нужны
    }

    public static <T> T read(String json, String path) {
        // Единая точка входа для чтения значений из JSON по JsonPath,
        // чтобы в тестах не плодились разные способы парсинга.
        return JsonPath.read(json, path);
    }

    public static void assertLenientEquals(String expected, String actual) throws Exception {
        // Lenient-сравнение: expected обязателен, но actual может содержать дополнительные поля.
        JSONAssert.assertEquals(expected, actual, false);
    }
}

И пример использования в тесте, где главный смысл всё ещё на поверхности:

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class JsonTestSupportUsageTest {

    @Test
    void should_extract_slug_from_response() {
        // Тело ответа в виде JSON: в тесте нам важно конкретное поле slug.
        String body = """
                {"slug":"java-basics","status":"DRAFT"}
                """;

        // Читаем поле по пути, не пряча смысл: прямо видно и JSON, и path.
        String slug = JsonTestSupport.read(body, "$.slug");

        // А ожидание формулируем в AssertJ-стиле.
        assertThat(slug).isEqualTo("java-basics");
    }
}

Почему это полезно именно для однородности набора тестов? Потому что вы стандартизируете мелкие операции, которые иначе каждый написал бы по-своему. Один человек использует JsonPath.read, другой — DocumentContext.parse, третий — руками через ObjectMapper (и заодно случайно меняет настройки). А так у вас есть один маленький и понятный путь, который не требует отдельного мини-курса по чтению.

8. Типичные ошибки при выборе стиля assertions

Ошибки в выборе assertion-стиля редко ломают приложение напрямую, но они отлично ломают вашу жизнь как автора тестов. Особенно неприятно то, что тесты могут оставаться зелёными, но становиться всё менее полезными: вы тратите время, а уверенности не прибавляется. Поэтому ошибки ниже — это не «смешные мелочи», а реальная причина, почему набор тестов в проектах превращается в тяжёлый груз.

Ошибка №1: выбирать библиотеку «по привычке», а не по форме результата.
Если тест вернул Java-объект, а вы упорно превращаете его в строку и сравниваете строку — вы усложняете себе задачу без выгоды. И наоборот: если тест вернул JSON, а вы сравниваете его как обычную строку, вы создаёте хрупкость. Правильный рефлекс звучит так: «что у меня на выходе?» — и только потом «чем проверять?».

Ошибка №2: смешивать AssertJ и Hamcrest в одном тесте так, что непонятно, где вы сейчас.
Особенно коварно то, что оба мира любят слово assertThat. Потом вы открываете тест, видите assertThat(...), а IDE показывает «не те методы», и начинается мини-детектив. Спасает дисциплина: AssertJ — через статический импорт, Hamcrest — через MatcherAssert.assertThat(...) явно. Тогда даже глазами видно, какой стиль применяется.

Ошибка №3: делать и JSONassert, и десяток JsonPath-проверок по одним и тем же полям в одном тесте.
Такой тест выглядит «очень надёжным», но на деле он просто многословный. При изменении контракта он падает в нескольких местах, а вы тратите время на починку дубликатов. Выберите ведущую стратегию: либо контракт целиком (JSONassert), либо отдельные правила (JsonPath + AssertJ).

Ошибка №4: использовать lenient JSON-сравнение там, где вам нужна строгая фиксация контракта.
Lenient хорош, когда вы осознанно сравниваете только часть. Но если тест должен защищать контракт DTO, lenient превращает тест в «я вроде что-то сравнил, но не очень понимаю, что именно доказал». В таких случаях строгость — ваш друг, потому что она ловит регрессии формы ответа, а не только смысл отдельных полей.

Ошибка №5: писать вспомогательные методы, которые скрывают смысл теста.
Помощники должны убирать повторяющийся шум, а не превращать тест в assertArticleIsOk(result), где внутри «немного магии, немного надежды и один случайный sleep» (ладно, sleep у нас ещё не было, но шутка пусть останется предупреждением). Если читатель не может по тесту понять, что проверяется, без похода в три утилитарных класса, — helper перешёл грань.

1
Задача
Spring Test, 3 уровень, 4 лекция
Недоступна
Выбор стиля по форме результата
Выбор стиля по форме результата
1
Задача
Spring Test, 3 уровень, 4 лекция
Недоступна
JsonPath и Hamcrest без смешения ролей
JsonPath и Hamcrest без смешения ролей
1
Опрос
AssertJ Проверки, 3 уровень, 4 лекция
Недоступен
AssertJ Проверки
Флуентные проверки коллекций и JSON
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ