JavaRush /Курсы /Spring Test /@DataJpaTest: data s...

@DataJpaTest: data slice для репозиториев

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

1. Роль repository‑тестов

После насыщенных дней с @WebMvcTest возникает ощущение: «Ну всё, мы же уже проверили API — значит система надежная». И вот тут репозиторий тихо улыбается в уголке, как кот, который только что уронил цветок, но делает вид, что это не он. Persistence layer ломается по‑своему: не так, как HTTP, не так, как JSON, и уж точно не так, как чистая бизнес‑логика в unit‑тестах.

В web‑тесте мы проверяем, что контроллер правильно принял запрос, вызвал сервис и вернул нужный статус/JSON. Но web‑тест обычно не доказывает, что данные реально сохраняются и читаются так, как мы ожидаем. Даже если вы мокали сервис аккуратно, всё равно остаётся огромный слой неизвестности: а JPA‑маппинг правильный? а nullable=false реально работает? а репозиторий действительно находит статью по slug, а не «по вдохновению»?

В ContentHub это особенно заметно на базовых вещах: Article хранит важные поля (slug, статус, автора, timestamps), и нам нужно доверять тому, что запись в БД и чтение из неё ведут себя стабильно. Поэтому мы вводим отдельный, честный и сфокусированный слой: repository‑tests.

2. Repository‑layer в ContentHub: не просто CRUD

Repository‑layer часто недооценивают, потому что Spring Data JPA выглядит как магия: написал интерфейс — и «само работает». Но это «само» работает ровно до того момента, пока вы не поменяли имя поля, не добавили ограничение, не обновили мэппинг связи или не сделали «очевидный» derived‑метод, который оказался не таким уж очевидным. И тогда баг проявляется не как красивое исключение с подсказкой, а как «ничего не находится», «падает при сохранении», «в проде другой SQL» и «почему оно прошло ревью?».

В нашем проекте ContentHub repository‑layer — это место, где живут интерфейсы вроде ArticleRepository, которые отвечают за чтение и запись Article (и позже — attachments, категории и прочее). Репозиторий — это граница между Java‑моделью и базой данных. Он обещает нам: «Ты даёшь сущность — я записываю её в таблицы. Ты даёшь критерий поиска — я возвращаю то, что есть в БД». И тесты на этом уровне нужны, чтобы проверять именно это обещание.

Для контекста, репозиторий обычно выглядит примерно так (сильно упрощённо, но по смыслу верно):

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ArticleRepository extends JpaRepository<Article, Long> {

    // Ищем статью по бизнес-ключу (slug), а не по техническому id
    // Spring Data сам сгенерирует запрос на основе имени метода
    Optional<Article> findBySlug(String slug);
}

Обратите внимание: здесь нет ни HTTP, ни DTO, ни контроллеров. Это «чистая» persistence‑граница. И тестировать её через @WebMvcTest — всё равно что проверять, как работает холодильник, открывая только дверцу и радуясь, что лампочка горит.

3. @DataJpaTest как test slice

В предыдущих модулях мы привыкли к мысли, что Spring Boot умеет поднимать узкие срезы контекста (slices), и это сильно ускоряет тесты и делает их честнее. Для JSON‑контрактов у нас был @JsonTest, для controller‑границы — @WebMvcTest. Логично, что для persistence‑границы тоже есть специализированный инструмент, а не «поднимем всё приложение и как‑нибудь проверим репозиторий внутри @SpringBootTest».

@DataJpaTest — это как раз такой инструмент: узкий data slice, который поднимает только то, что нужно для JPA и репозиториев. Он нужен, чтобы быстро и предсказуемо проверять, как ведут себя @Entity, их мэппинг и Spring Data repositories. То есть мы проверяем не «приложение в целом», а конкретную подсистему: persistence layer.

И это важное психологическое переключение: @DataJpaTest — не про «ну хоть какой‑то тест». Это про дисциплину: у каждого слоя есть своя зона доказательства. Data slice доказывает, что слой хранения данных живой, адекватный и не врёт нам в лицо (по крайней мере — не специально).

4. Состав контекста @DataJpaTest

Когда вы ставите на тестовый класс @DataJpaTest, Spring Boot собирает маленький контекст, который фокусируется на JPA. Полезно представлять себе это не как «аннотацию», а как «режим запуска тестовой среды». В этом режиме вы получаете всё, что нужно, чтобы реально работать с базой данных через JPA, но не получаете лишнего, что замедляет suite и смешивает ответственности.

Давайте зафиксируем это в виде небольшой таблицы — она часто спасает от самых типичных ожиданий в духе «а почему мой сервис не инжектится? он же такой хороший»:

Часть приложения В @DataJpaTest по умолчанию Комментарий человеческим языком
@Entity классы (JPA сущности) Да Иначе нечего маппить в таблицы
Spring Data JPA repositories (JpaRepository и т.п.) Да Это главный объект проверки
EntityManager / JPA инфраструктура Да Hibernate (или другой провайдер) должен работать
DataSource Да Нужна база, пусть даже тестовая
Транзакции для тестов Обычно да Как правило, тесты идут в транзакции (подробности — следующая лекция)
@Controller, @RestController Нет Это web‑слой, он здесь лишний
@Service и orchestration‑логика Нет Это бизнес‑слой, его тестируют иначе
Security config / filters Нет Это отдельный слой риска, не смешиваем
MockMvc, HTTP, сериализация response Нет Это было в @WebMvcTest

На практике это означает: @DataJpaTest — очень быстрый способ сказать Spring: «Подними мне ровно столько приложения, чтобы я мог проверить репозитории и JPA‑маппинг. И пожалуйста, не тащи сюда контроллеры, безопасность и весь зоопарк конфигураций».

И ещё один важный момент: если в classpath есть драйвер embedded database (например, H2), Spring Boot часто может поднять тестовую базу автоматически. Это удобно для старта и быстрых проверок. При этом мы не делаем вид, что embedded БД на 100% равна PostgreSQL — просто используем её как быстрый тренировочный полигон. Но без забегания вперёд: сегодня нам важно понять сам slice и его границы.

5. Минимальный каркас data‑slice теста

Переход от web‑тестов к data‑тестам обычно ощущается как смена профессии: вчера вы собирали HTTP‑запросы, сегодня вы внезапно думаете про сущности и таблицы. Это нормально. Хорошая новость: стартовать можно очень просто. @DataJpaTest делает большую часть тяжёлой работы за вас, а от вас требуется аккуратная структура теста и ясное понимание «что именно я проверяю».

Минимальный каркас теста для репозитория выглядит так:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest // Поднимаем только JPA-инфраструктуру и Spring Data репозитории
class ArticleRepositoryDataJpaTest {

    @Autowired
    private ArticleRepository articleRepository; // Это "объект проверки": реальный репозиторий, не мок
}

И это не «заготовка ради галочки». Уже на этом этапе важно заметить: мы не поднимаем ContentHubApplication, не запускаем сервер, не трогаем контроллеры. Это тест уровня persistence.

Если вы привыкли, что в @WebMvcTest вы подменяли сервисы через @MockitoBean, то здесь идея другая: вы не мокаете репозиторий, вы проверяете реальное поведение репозитория. Мокать репозиторий в repository‑тесте — это как тестировать велосипед, рисуя его маркером на бумаге. Формально колёса круглые, но ехать не получается.

6. Тест: сохраняем Article

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

Простейший сценарий выглядит так: создаём валидную Article, сохраняем, убеждаемся, что у сохранённой сущности появился id. Появление id — это не косметика. Это минимальная sanity‑проверка, что сущность успешно проходит через JPA‑слой и получает первичный ключ. Но это ещё не тот случай, где мы специально доказываем физический round‑trip до БД: внутри одного persistence context JPA многое держит под своим контролем. Пока нам важно увидеть более простой факт — data slice живой и репозиторий умеет сохранить валидную сущность. Когда понадобится жёстко отделить managed‑состояние от реально записанных в БД изменений, в ход пойдут flush и clear.

Пример (с небольшим helper‑методом для читаемости):

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

import org.junit.jupiter.api.Test;

@Test
void savesArticleInsideDataSlice() {
    // Act: сохраняем сущность через реальный Spring Data репозиторий
    Article saved = articleRepository.save(validArticle("spring-data-basics"));

    // Assert: если id появился — значит, базовое сохранение через JPA-слой отработало
    assertThat(saved.getId()).isNotNull();
}

Здесь validArticle(...) — просто shorthand для минимально валидной статьи ContentHub. В рабочем тестовом классе такую фикстуру удобнее собирать через отдельные helper‑методы и сразу вместе с категорией, чтобы все тесты опирались на одну и ту же валидную модель статьи.

7. Embedded database в @DataJpaTest: автоподъём без паники

Когда вы впервые запускаете @DataJpaTest, бывает два типичных чувства. Первое: «О, оно само подняло базу — я гений». Второе (обычно через 30 секунд): «Подождите, какую базу оно подняло? Я ничего не настраивал». Оба чувства нормальные. Spring Boot действительно старается сделать data slice максимально удобным: если в тестовом classpath есть embedded БД, он может автоматически использовать её как тестовую.

Идея здесь простая: repository‑тесты должны быть быстрыми и локальными, иначе они перестают быть репозиторными тестами и превращаются в «мини‑интеграцию со всем миром». Embedded database помогает получить быструю обратную связь и поймать базовые проблемы мэппинга и репозиторных методов, не требуя от студента (и от команды) тяжелой инфраструктуры прямо с первой же лекции data‑модуля.

Но важно удерживать правильное ожидание: embedded БД — это учебный и быстрый режим, а не «абсолютная истина про прод». Мы используем её как удобный старт, чтобы понять механику и научиться писать аккуратные repository‑tests. Реальные различия между базами — отдельная тема, и мы не будем превращать первую лекцию data‑дня в «войну диалектов SQL».

8. Границы data slice

Очень легко сделать data‑slice тест «универсальным комбайном»: подтянуть сервисы, дернуть workflow, проверить бизнес‑переходы статусов, а заодно убедиться, что контроллер возвращает 200 OK. Технически вы можете это попробовать, но смысл slice‑подхода будет уничтожен примерно так же, как смысл диеты уничтожается фразой «ну это же маленький кусочек торта». Data‑slice существует именно затем, чтобы не смешивать разные зоны ответственности.

В @DataJpaTest мы не проверяем HTTP‑контракты, не сравниваем JSON, не оцениваем сообщения валидации и не проверяем работу @ControllerAdvice. Всё это уже было (и было правильно) в web‑слое. Мы также не переносим сюда правила PublicationPolicy и orchestration из ArticleWorkflowService, потому что они относятся к бизнес‑логике и покрываются unit‑тестами и (позже) интеграционными сценариями.

Data‑slice тест отвечает на более приземлённые, но критически важные вопросы: «Могу ли я сохранить сущность в БД?», «Могу ли я достать её обратно?», «То, что я ищу по ключевому полю (например, slug), действительно находится?». И в этом есть своя красота: тест становится коротким, честным и очень понятным в диагностике. Он падает не потому, что «всё приложение не завелось», а потому что конкретная часть persistence‑слоя ведёт себя не так.

9. Типичные ошибки при первом знакомстве с @DataJpaTest

Ошибка №1: ожидать, что в @DataJpaTest будут доступны контроллеры и сервисы.
Это частая привычка после @SpringBootTest: хочется, чтобы «всё было под рукой». Но data slice специально ограничивает контекст. Если вам нужен сервис — это уже другой уровень теста. Если вы тащите сервис в data‑тест, вы рискуете начать проверять бизнес‑логику через БД, а это дорогой и запутанный путь.

Ошибка №2: пытаться тестировать HTTP‑семантику в repository‑тесте.
В репозиторном тесте нет статусов 200/400, нет JSON, нет MockMvc. И это не «неудобство», а защита от смешения слоев. Если вы ловите себя на желании проверить Content-Type в @DataJpaTest, скорее всего, вы просто открыли не ту дверь.

Ошибка №3: импортировать в @DataJpaTest половину конфигурации «чтобы заработало».
Иногда студент видит ошибку биндинга и решает: «Ладно, добавлю @Import(MyHugeConfig.class)». Тест действительно может заработать, но вы потеряете главное преимущество slice‑тестов: скорость, локальность и понятные границы. В итоге получаете маленький @SpringBootTest, только хуже и без честного названия.

Ошибка №4: замокать repository в repository‑тесте.
Это выглядит парадоксально, но встречается: «Я привык мокать зависимости, давайте замокаем ArticleRepository». Тогда тест будет проверять… ваш мок. То есть вашу фантазию о том, как должен вести себя репозиторий. Это неплохой жанр литературы, но слабый жанр тестирования.

Ошибка №5: писать один огромный тест “на всё” вместо нескольких маленьких.
Большой тест быстро превращается в плохо читаемый сценарий, который падает «где-то в середине». На data‑слое особенно важно держать тесты короткими: setup — действие — проверка. Чем меньше в тесте лишней истории, тем проще понять, что реально сломалось.

1
Задача
Spring Test, 14 уровень, 0 лекция
Недоступна
Сохранение книги в data slice
Сохранение книги в data slice
1
Задача
Spring Test, 14 уровень, 0 лекция
Недоступна
Поиск подписчика по email в `@DataJpaTest`
Поиск подписчика по email в `@DataJpaTest`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ