Введение
Всем привет!
Тестирование — это тот самый этап разработки, где магия превращения кода в надёжное решение действительно происходит. Иногда мы пишем простые тесты, а иногда сталкиваемся с такими сценариями, где недостаточно проверить результат — нужно глубже разобраться, что происходит «за кулисами».
Например, вы хотите удостовериться, что ваш сервис корректно взаимодействует с внедрённым репозиторием, вызывая нужные методы с правильными аргументами. При этом вы хотите сохранить работу с реальной базой данных, чтобы не терять контекст. Тут на сцену выходит @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
и удостовериться, что:
-
метод
existsByUsername
вызывается один раз; -
метод
save
вызывается только если пользователь отсутствует; -
пользователь действительно сохраняется в базу данных;
-
сохранённый объект корректен и доступен через метод
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)); } }
Разбор теста:
-
Проверка вызовов методов — проверяем, что методы
existsByUsername
иsave
вызываются корректно; -
Сохранение данных — с помощью метода
findByUsername
убеждаемся, что пользователь действительно сохранился в базу данных; -
Гибкость
@SpyBean
— он позволяет комбинировать наблюдение за поведением объекта и работу с реальными данными.
Советы и подводные камни
-
Не путайте
@SpyBean
и@MockBean
—@SpyBean
используется, когда вам нужно сохранить функциональность объекта. -
Cледите за производительностью — использование реальных объектов может замедлить тесты, особенно если они работают с базой данных.
-
Доверяйте, но проверяйте — убедитесь, что ваши тесты актуальны и соответствуют текущему поведению системы.
Заключение
@SpyBean
— это мощный инструмент для тестирования в Spring Boot, который позволяет отслеживать вызовы методов реальных объектов в вашем приложении, не мокая их полностью. Этот подход полезен, когда вам нужно проверить поведение компонентов без нарушения их логики и функциональности.
С помощью @SpyBean
можно отслеживать вызовы в сервисах, репозиториях, контроллерах, аспектах и многих других компонентах приложения.
Пусть ваши тесты будут надежными, а код — безупречным. Всем удачи в написании качественного кода без багов! 😊
ссылка на оригинал статьи https://habr.com/ru/articles/860786/
Добавить комментарий