Введение
Всем привет!
Тестирование — это тот самый этап разработки, где магия превращения кода в надёжное решение действительно происходит. Иногда мы пишем простые тесты, а иногда сталкиваемся с такими сценариями, где недостаточно проверить результат — нужно глубже разобраться, что происходит "за кулисами".
Например, вы хотите удостовериться, что ваш сервис корректно взаимодействует с внедрённым репозиторием, вызывая нужные методы с правильными аргументами. При этом вы хотите сохранить работу с реальной базой данных, чтобы не терять контекст. Тут на сцену выходит @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
можно отслеживать вызовы в сервисах, репозиториях, контроллерах, аспектах и многих других компонентах приложения.
Пусть ваши тесты будут надежными, а код — безупречным. Всем удачи в написании качественного кода без багов! 😊