Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров4.2K

Введение

Всем привет!

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

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

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

Теги:
Хабы:
Всего голосов 3: ↑2 и ↓1+1
Комментарии8

Публикации

Работа

Java разработчик
178 вакансий

Ближайшие события