JavaRush /Курсы /Spring Test /@AutoConfigureTestDatabase

@AutoConfigureTestDatabase: replace = NONE

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

1. embedded database иногда врёт

Пока вы учитесь писать @DataJpaTest, встроенная база выглядит как идеальный напарник: запускается быстро, ничего не нужно поднимать руками, тесты летят как реактивный самолёт. Но у этой идиллии есть побочный эффект: embedded database — это не «маленький PostgreSQL», а вообще другая база, со своим характером, привычками и пониманием слов «уникальность», «сортировка» и «дата».

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

Embedded database — это честный инструмент для скорости и обратной связи. Просто важно помнить: она доказывает корректность репозиторной логики в целом, но не всегда доказывает корректность поведения именно PostgreSQL, с которым живёт ваш ContentHub в продакшне.

Здесь важно не промахнуться по масштабу: мы не меняем тип теста и не строим сейчас отдельный инфраструктурный трек. Это всё тот же @DataJpaTest для repository-layer; меняется только вопрос, какая база стоит под ним и когда сама среда становится частью риска.

2. @DataJpaTest и подмена DataSource

@DataJpaTest: откуда берётся база данных

Чтобы осознанно управлять выбором базы, полезно понимать: @DataJpaTest — это slice. Он поднимает JPA-инфраструктуру и репозитории, но при этом Spring Boot пытается сделать тест максимально «лёгким» и самостоятельным. Один из способов — автоматически подменить ваш DataSource на встроенный, если он доступен в classpath.

Практически это выглядит так: у вас в приложении в application.yml настроен PostgreSQL, но в тестах, если рядом лежит dependency на embedded DB (часто H2), Boot говорит: «О, у нас есть in-memory база, давай я подключу её, чтобы тесты не зависели от внешнего сервиса». И подключает.

Если представить это в виде небольшой схемы, получится примерно так:

flowchart TD
    A["@DataJpaTest стартует"] --> B{Есть embedded DB driver в classpath?}
    B -- "Да" --> C["Boot заменяет DataSource на embedded database"]
    B -- "Нет" --> D["Boot использует DataSource из конфигурации теста/проекта"]
    C --> E["Тесты бегут быстро, но база может отличаться от PostgreSQL"]
    D --> F["Тесты ближе к прод-среде, но требуют реальной БД"]

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

@AutoConfigureTestDatabase: управление подменой DataSource

Аннотация @AutoConfigureTestDatabase — это не про JPA, не про репозитории и не про «супер-режим тестов». Это очень приземлённая штука: она управляет тем, будет ли Spring Boot пытаться заменить DataSource на embedded database.

Важный момент: @DataJpaTest обычно и так включает поведение «подменить базу, если можно». Поэтому, когда вы добавляете @AutoConfigureTestDatabase(replace = NONE), вы не «включаете real DB», вы запрещаете подмену.

У @AutoConfigureTestDatabase есть режимы Replace. Нам сейчас важны в первую очередь два: дефолтный (подмена разрешена) и NONE (подмена запрещена). Чтобы не держать это в голове как заклинание, удобно один раз увидеть это в таблице:

Режим Replace Что делает Boot в тесте Простая интерпретация
ANY Ставит embedded DB, если она доступна Если можно упростить — упростим
NONE Не заменяет DataSource Тестируйся на той базе, что настроена

На практике переход выглядит так: было (embedded path по умолчанию), стало (real DB path через конфигурацию).

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest // Поднимаем только JPA-срез: репозитории и инфраструктуру, без всего приложения
class ArticleRepositoryDataJpaTest {
    // Здесь Boot может подменить DataSource на embedded БД (если она есть в classpath)
}

И вариант «не подменять»:

import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest // Всё ещё JPA-slice, а не полноценный @SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // Запрещаем Boot подменять DataSource на embedded БД
class ArticleRepositoryRealDbTest {
    // После этого DataSource будет взят из вашей конфигурации (например, test-профиля)
}

Обратите внимание на важную психологическую деталь: это всё ещё @DataJpaTest. Вы не превратили тест в @SpringBootTest. Вы всё ещё тестируете persistence-layer узко и относительно быстро. Вы просто поменяли «какая база стоит под ним».

И это не новый дефолт для всего suite. Большая часть repository-тестов по‑прежнему прекрасно живёт на embedded path и даёт быструю обратную связь; replace = NONE нужен там, где риск уже сидит именно в поведении конкретной базы.

3. replace = NONE на практике

Самая полезная мысль здесь немного скучная, но очень инженерная: при переходе на replace = NONE логика теста обычно не меняется. Вы не должны внезапно переписывать assertions, добавлять новые проверки или менять AAA-структуру. Вы меняете не тест, а среду, в которой он исполняется.

Если тест проверял «репозиторий находит опубликованную статью по slug», то он и дальше проверяет это же. Просто теперь вы получаете ответ не от H2/HSQLDB/Derby, а от реального PostgreSQL (или от той БД, которая у вас настроена как настоящая база проекта).

Вот пример в стиле ContentHub, максимально приземлённый. Пусть у нас есть метод, который читает статью по slug и статусу:

import java.util.Optional;

import org.springframework.data.repository.Repository;

interface ArticleRepository extends Repository<Article, Long> {

    // Ищем статью по бизнес-ключу (slug) и статусу публикации
    Optional<Article> findBySlugAndStatus(String slug, ArticleStatus status);
}

А теперь тест. Он одинаково выглядит и для embedded, и для real DB path:

import org.junit.jupiter.api.Test; 
import org.springframework.beans.factory.annotation.Autowired;

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

class ArticleRepositoryExampleTest {

    @Autowired
    ArticleRepository repository; // Репозиторий тестируем напрямую, без сервиса/контроллера

    @Test
    void findsPublishedArticleBySlug() {
        // Действие: вызываем метод репозитория так, как это сделает реальный код
        var result = repository.findBySlugAndStatus(
                "spring-boot-testing",
                ArticleStatus.PUBLISHED
        );

        // Проверка: для учебного примера достаточно убедиться, что статья найдена
        assertThat(result).isPresent();
    }
}

Да, в реальности вам нужно подготовить данные через TestEntityManager.persist(...), потом сделать flush() и clear(). Но смысл в другом: сам тест не становится «другим тестом». Он просто получает более сильное доказательство.

И ещё один момент, который часто удивляет новичков: тесты с replace = NONE в @DataJpaTest всё равно обычно транзакционные и откатываются. То есть вы не «засоряете» базу бесконечными данными после каждого теста — пока у вас нет намеренного коммита и специальных настроек.

4. Чем embedded DB отличается от PostgreSQL

Когда говорят «embedded database может врать», это не означает «она случайно возвращает неправильные данные». Обычно речь о том, что некоторые граничные нюансы ведут себя иначе. В реальном проекте именно эти нюансы и становятся регрессиями, потому что их трудно увидеть глазами в коде и легко пропустить в ревью.

Один из самых частых классов расхождений — это DDL и constraints. Ваши миграции (Flyway) и схема в PostgreSQL могут быть богаче: типы, ограничения, индексы, нюансы varchar-длин, поведение unique constraints. Embedded DB иногда прощает то, что PostgreSQL не простит, или наоборот: «ругается» на DDL, который PostgreSQL принимает спокойно. Итог — вы либо получаете ложную уверенность, либо ложную тревогу.

Вторая большая зона — типы даты/времени и их точность. PostgreSQL и embedded базы по-разному хранят timestamps, по-разному округляют микросекунды/наносекунды, по-разному относятся к timezone. Если в ContentHub порядок публикаций зависит от publishedAt, вы можете поймать очень неприятный баг: сортировка «плавает», потому что в одной базе время хранится с одной точностью, а в другой — с другой. Особенно коварно, когда вы создаёте много записей «почти одновременно».

Третья зона — сортировка, NULL и стабильность порядка. PostgreSQL имеет свои правила сортировки NULL значений (например, в некоторых режимах NULL может быть «самым большим» и уходить в конец/начало в зависимости от направления сортировки). Embedded DB может сортировать иначе. Если ваш query строит public feed и вы ожидаете стабильный порядок, различия в NULLS FIRST/LAST или в сравнении строк могут проявиться только на реальной базе.

Четвёртая зона — генерация ID и последовательности. JPA старается абстрагировать вас от того, как именно генерируются ключи, но на практике IDENTITY, SEQUENCE и «как база реально создаёт значения» иногда влияет на порядок вставки, на то, когда именно появляется ID, и на некоторые граничные сценарии вокруг batch-операций. Это редко «ломает всё», но иногда ломает тесты, которые ожидали один порядок действий, а получили другой.

И наконец, пятая зона — диалект и SQL-особенности. JPQL обычно переносим, но generated SQL (особенно для pagination, limit/offset, сортировки, некоторых функций) зависит от диалекта. В embedded DB вы могли получить один SQL и поведение, а в PostgreSQL — другой. Если вы тестируете важный запрос (например, публичную выдачу опубликованных статей), эти различия как раз и должны быть пойманы максимально близко к реальности.

Важно: не нужно запоминать этот абзац как список заклинаний. Достаточно одной идеи — у разных баз разные привычки, и replace = NONE позволяет проверить поведение на базе, которая важна именно вашему проекту.

5. Минимальная конфигурация real DB

Сама аннотация @AutoConfigureTestDatabase(replace = NONE) не создаёт вам PostgreSQL из воздуха. Она лишь говорит Boot: «не подменяй datasource». Поэтому вы должны понимать, откуда возьмётся DataSource, если embedded-замены больше нет.

Обычно в учебном проекте это решается через test-профиль, который вы уже проходили: application-test.yml в src/test/resources или src/main/resources (в зависимости от правил проекта), и явное включение профиля test. Здесь мы не обсуждаем, как поднять PostgreSQL (Docker, Compose и так далее) — мы просто фиксируем принцип: тест подключится к тому datasource, который вы настроили.

Условный пример src/test/resources/application-test.yml для PostgreSQL может выглядеть так:

spring:
  datasource:
    # JDBC URL тестовой БД: тесты будут подключаться именно сюда
    url: jdbc:postgresql://localhost:5432/contenthub
    username: contenthub
    password: contenthub
  jpa:
    hibernate:
      # В тестах полезно валидировать схему, чтобы ловить расхождения с миграциями
      ddl-auto: validate

Если вы используете профили (а в курсе мы их используем), тесту часто нужен @ActiveProfiles("test"), чтобы этот YAML реально применился:

import org.springframework.test.context.ActiveProfiles;

@ActiveProfiles("test") // Включаем test-профиль, чтобы подхватился application-test.yml
class SomeDataTest {
    // Здесь не показаны аннотации тестового среза — важен именно профиль
}

Ещё раз: сейчас важно не «всё настроить идеально», а понять, что replace = NONE делает тест более честным, но требует от вас более честной среды. Иначе контекст может просто не подняться, потому что он не нашёл рабочий datasource.

6. Типичные ошибки при replace = NONE

Ошибка №1: воспринимать replace = NONE как “сделай тест профессиональным”.
Иногда кажется, что достаточно добавить одну аннотацию — и всё, вы «взрослый разработчик». На практике это просто переключатель среды. Если тест сам по себе слабый (например, проверяет только hasSize(2) без проверки состава и порядка), то на PostgreSQL он останется таким же слабым — просто будет медленнее исполняться и громче падать.

Ошибка №2: путать “реальная база” с “полный контекст приложения”.
@DataJpaTest с replace = NONE — это всё ещё data-slice. Вы не тестируете контроллеры, сервисы и security. Если тест вдруг начинает «неожиданно требовать» половину приложения, значит вы случайно вышли за границы слоя (например, импортировали лишнюю конфигурацию или потянули зависимости не из persistence-мира).

Ошибка №3: менять assertions, чтобы тест “проходил” на новой базе.
Очень опасная привычка: переключились на PostgreSQL, тест упал из‑за реального поведения, и вы вместо анализа просто «подкрутили ожидание». Так легко легализовать баг. Если меняется результат — сначала сформулируйте, почему. Это дефект? Особенность? Неправильный order-by? Проблема с NULL? Только потом меняйте тест или код.

Ошибка №4: забыть, что теперь datasource берётся из конфигурации — и получить падение контекста.
На embedded path многое работало «из коробки», и вы могли не задумываться, откуда берётся база. С replace = NONE тест честно попытается подключиться к реальному datasource. Если конфигурации нет или база недоступна, тест упадёт ещё на старте. Это нормально: инструмент не сломан, просто вы попросили реальность.

Ошибка №5: переносить на real DB path всё подряд, включая самые простые запросы.
Даже если вы хотите «максимальной честности», цена тестов важна. Реальная база обычно медленнее, требует больше дисциплины окружения и может ухудшить скорость обратной связи. Гораздо разумнее сначала понять, что именно даёт риск, а потом уже решать, какие тесты действительно должны жить на replace = NONE. (Стоп — дальше мы сейчас не идём, чтобы не забежать вперёд темы дня.)

1
Задача
Spring Test, 16 уровень, 3 лекция
Недоступна
Data-slice тест без подмены DataSource
Data-slice тест без подмены DataSource
1
Задача
Spring Test, 16 уровень, 3 лекция
Недоступна
Проверка уникальности slug на real DB path
Проверка уникальности slug на real DB path
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ