Работа с @SpyBean: использование в Spring Boot

от автора

Введение

Всем привет!

Тестирование — это тот самый этап разработки, где магия превращения кода в надёжное решение действительно происходит. Иногда мы пишем простые тесты, а иногда сталкиваемся с такими сценариями, где недостаточно проверить результат — нужно глубже разобраться, что происходит «за кулисами».

Например, вы хотите удостовериться, что ваш сервис корректно взаимодействует с внедрённым репозиторием, вызывая нужные методы с правильными аргументами. При этом вы хотите сохранить работу с реальной базой данных, чтобы не терять контекст. Тут на сцену выходит @SpyBean — универсальный инструмент для подобных задач.

В этой статье рассматривается правильное использование аннотации @SpyBean. Разбирается реальный сценарий с базой данных, а также показано, как с её помощью можно сделать тесты более мощными и точными.

Когда и зачем нужен @SpyBean?

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

  • сколько раз вызывался метод?

  • с какими аргументами происходил вызов?

  • корректно ли обработались результаты вызова?

  • данные действительно сохраняются в базу, и их можно корректно извлечь?

Здесь @SpyBean идеально подходит, поскольку он позволяет работать с реальными объектами и сохраняет их функциональность, добавляя возможность наблюдать за их поведением.

Подготовка окружения

Для реализации всех примеров из статьи вам потребуется настроить зависимости в build.gradle. Вот их минимальный набор:

dependencies {     implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '3.3.5'      testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '3.3.5'     testImplementation group: 'com.h2database', name: 'h2', version: '2.2.224' } 

Что делает каждая зависимость:

  • spring-boot-starter-data-jpa: предоставляет функционал для работы с базой данных через Spring Data JPA;

  • spring-boot-starter-test: включает всё необходимое для тестирования: JUnit, Spring TestContext и поддержку Mockito;

  • h2: встроенная реляционная база данных, удобная для тестирования и быстрого прототипирования.

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

Давайте рассмотрим сценарий, где сервис взаимодействует с базой данных через репозиторий. Мы хотим проверить, как сервис вызывает методы репозитория, сохраняя при этом реальное поведение.

Класс User

import jakarta.persistence.*;  import java.util.Objects;  @Entity @Table(name = "user_data") public class UserData {      @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      @Column     private String username;      @Column     private String email;      @Column     private String password;      public UserData() {     }      public UserData(String username, String email, String password) {         this.username = username;         this.email = email;         this.password = password;     }      public String getUsername() {         return username;     }      @Override     public boolean equals(Object o) {         if (this == o) return true;         if (o == null || getClass() != o.getClass()) return false;         UserData userData = (UserData) o;         return Objects.equals(username, userData.username) && Objects.equals(email, userData.email) && Objects.equals(password, userData.password);     }  } 

Сервис UserService

import org.springframework.stereotype.Service;  @Service public class UserDataService {      private final UserDataRepository userDataRepository;      public UserDataService(UserDataRepository userDataRepository) {         this.userDataRepository = userDataRepository;     }      public void createUser(UserData userData) {         if (userDataRepository.existsByUsername(userData.getUsername())) {             throw new IllegalArgumentException("UserData already exists");         }         userDataRepository.save(userData);     } } 

Репозиторий UserRepository

import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository;  import java.util.Optional;  @Repository public interface UserDataRepository extends JpaRepository<UserData, Long> {     boolean existsByUsername(String username);      Optional<UserData> findByUsername(String username); } 

Задача для теста:

Мы хотим протестировать метод createUser и удостовериться, что:

  1. метод existsByUsername вызывается один раз;

  2. метод save вызывается только если пользователь отсутствует;

  3. пользователь действительно сохраняется в базу данных;

  4. сохранённый объект корректен и доступен через метод findByUsername .

Тест с использованием @SpyBean

Для корректной работы с базой данных в тестах необходимо создать таблицу для хранения пользователей. В качестве базы данных для тестов мы будем использовать H2, так как она отлично подходит для быстрого прототипирования и тестирования. Ниже приведён пример скрипта для создания таблицы user_data:

Создадим файл src/test/resources/data.sql, в котором будет прописан SQL-запрос для создания таблицы user_data и добавления тестовых данных:

CREATE TABLE IF NOT EXISTS user_data (     id serial PRIMARY KEY,     username VARCHAR(255) NOT NULL UNIQUE,     email VARCHAR(255) NOT NULL,     password VARCHAR(255) NOT NULL );  INSERT INTO user_data (username, email, password) VALUES ('bob', 'bob@example.com', 'password123'), ('jane', 'jane@example.com', 'password456');

Реализация теста

import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.jdbc.Sql;  import java.util.Optional;  import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*;  @Sql("classpath:data.sql") @SpringBootTest @AutoConfigureTestDatabase class UserDataServiceTest {      @Autowired     private UserDataService userDataService;      @SpyBean     private UserDataRepository userDataRepository;      @Test     void createUser_whenUserDoesNotExist_savesUser() {         UserData userData = new UserData("new_user", "new_email", "new_password");         userDataService.createUser(userData);          // Проверка: метод existsByUsername был вызван 1 раз         verify(userDataRepository, times(1)).existsByUsername(userData.getUsername());          // Проверка: метод save был вызван с правильным объектом         verify(userDataRepository, times(1)).save(userData);          // Проверка: пользователь действительно сохранился в базе         Optional<UserData> savedUserOptional = userDataRepository.findByUsername(userData.getUsername());         assertTrue(savedUserOptional.isPresent());         assertEquals(userData, savedUserOptional.get());     }      @Test     void createUser_whenUserAlreadyExists_throwsException() {         UserData userData = new UserData("bob", "any_email", "any_password");          // Проверка: исключение         assertThrows(IllegalArgumentException.class, () -> userDataService.createUser(userData));          // Проверка: метод existsByUsername был вызван 1 раз         verify(userDataRepository, times(1)).existsByUsername(userData.getUsername());          // Проверка: метод save не был вызван         verify(userDataRepository, never()).save(any(UserData.class));     } } 

Разбор теста:

  1. Проверка вызовов методов — проверяем, что методы existsByUsername и save вызываются корректно;

  2. Сохранение данных — с помощью метода findByUsername убеждаемся, что пользователь действительно сохранился в базу данных;

  3. Гибкость @SpyBean — он позволяет комбинировать наблюдение за поведением объекта и работу с реальными данными.

Советы и подводные камни

  1. Не путайте @SpyBean и @MockBean@SpyBean используется, когда вам нужно сохранить функциональность объекта.

  2. Cледите за производительностью — использование реальных объектов может замедлить тесты, особенно если они работают с базой данных.

  3. Доверяйте, но проверяйте — убедитесь, что ваши тесты актуальны и соответствуют текущему поведению системы.

Заключение

@SpyBean — это мощный инструмент для тестирования в Spring Boot, который позволяет отслеживать вызовы методов реальных объектов в вашем приложении, не мокая их полностью. Этот подход полезен, когда вам нужно проверить поведение компонентов без нарушения их логики и функциональности.

С помощью @SpyBean можно отслеживать вызовы в сервисах, репозиториях, контроллерах, аспектах и многих других компонентах приложения.

Пусть ваши тесты будут надежными, а код — безупречным. Всем удачи в написании качественного кода без багов! 😊


ссылка на оригинал статьи https://habr.com/ru/articles/860786/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *