← Назад к вопросам

Что такое Extension в Spring Boot?

2.0 Middle🔥 181 комментариев
#Stream API и функциональное программирование#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Extensions в Spring Boot и JUnit 5: расширяемость фреймворка

Extensions (расширения) — это механизм в JUnit 5 для расширения функциональности фреймворка. Spring Boot активно использует Extensions для интеграции с контейнером Spring в тестах. Это современная замена для Rules из JUnit 4.

Основные концепции

Extension — это интерфейс, который позволяет перехватывать и модифицировать жизненный цикл тестов:

// Самые распространённые типы Extensions:

// 1. BeforeEachCallback - выполнить перед каждым тестом
// 2. AfterEachCallback - выполнить после каждого теста
// 3. BeforeAllCallback - выполнить один раз перед всеми тестами
// 4. AfterAllCallback - выполнить один раз после всех тестов
// 5. ParameterResolver - предоставить параметры для методов
// 6. TestInstancePreProcessor - модифицировать экземпляр теста
// и другие...

Spring Boot Extension

Dля тестов Spring Boot используется @SpringBootTest аннотация, которая регистрирует SpringExtension:

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

// Способ 1: явная регистрация
@SpringBootTest
class MyServiceTest {
    
    @Test
    void testSomething() {
        // Контекст Spring уже загружен
    }
}

// Способ 2: явно регистрируем расширение
@ExtendWith(SpringExtension.class)
class MyServiceTestExplicit {
    
    @Test
    void testSomething() {
        // Контекст Spring загружен через расширение
    }
}

Практический пример: создание собственного Extension

1. Логирование тестов

import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingExtension implements BeforeEachCallback, AfterEachCallback {
    private static final Logger logger = LoggerFactory.getLogger(LoggingExtension.class);
    
    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        String testName = context.getDisplayName();
        logger.info("Запуск теста: {}", testName);
    }
    
    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        String testName = context.getDisplayName();
        logger.info("Завершён тест: {}", testName);
    }
}

// Использование:
@ExtendWith(LoggingExtension.class)
class UserServiceTest {
    
    @Test
    void testCreateUser() {
        // Будет залогировано:
        // INFO: Запуск теста: testCreateUser
        // ... выполнение теста ...
        // INFO: Завершён тест: testCreateUser
    }
}

2. Extension для управления тестовыми данными

import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;

public class DatabaseCleanupExtension implements BeforeEachCallback {
    
    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        // Получаем Spring контекст из расширения
        ApplicationContext appContext = SpringExtension
            .getApplicationContext(context);
        
        // Получаем бин репозитория
        UserRepository userRepository = appContext
            .getBean(UserRepository.class);
        
        // Очищаем БД перед каждым тестом
        userRepository.deleteAll();
    }
}

// Использование:
@SpringBootTest
@ExtendWith(DatabaseCleanupExtension.class)
class UserRepositoryTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testSaveUser() {
        // БД уже очищена благодаря расширению
        User user = new User("John", "john@example.com");
        userRepository.save(user);
        
        assertEquals(1, userRepository.count());
    }
}

3. Extension для предоставления тестовых данных (ParameterResolver)

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import java.lang.reflect.Parameter;

public class TestUserProviderExtension implements ParameterResolver {
    
    @Override
    public boolean supportsParameter(ParameterContext parameterContext,
                                     ExtensionContext extensionContext) {
        // Проверяем, является ли параметр типом TestUser
        Parameter parameter = parameterContext.getParameter();
        return parameter.getType() == TestUser.class;
    }
    
    @Override
    public Object resolveParameter(ParameterContext parameterContext,
                                   ExtensionContext extensionContext) {
        // Создаём и возвращаем тестового пользователя
        return new TestUser("testuser", "test@example.com", "password123");
    }
}

public class TestUser {
    public final String username;
    public final String email;
    public final String password;
    
    public TestUser(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }
}

// Использование:
@ExtendWith(TestUserProviderExtension.class)
class AuthenticationTest {
    
    @Test
    void testUserLogin(TestUser testUser) {
        // testUser автоматически предоставлен расширением
        assertEquals("testuser", testUser.username);
    }
}

Spring Boot встроенные Extensions

1. @SpringBootTest (содержит SpringExtension)

@SpringBootTest
class ApplicationTest {
    
    @Autowired
    private ApplicationContext context;
    
    @Test
    void contextLoads() {
        assertNotNull(context);
    }
}

2. @DataJpaTest (для тестирования репозиториев)

@DataJpaTest
class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testSaveUser() {
        User user = new User("John");
        entityManager.persistAndFlush(user);
        
        User found = userRepository.findById(user.getId()).orElse(null);
        assertNotNull(found);
    }
}

3. @WebMvcTest (для тестирования контроллеров)

@WebMvcTest(UserController.class)
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void testGetUser() throws Exception {
        User user = new User("John");
        when(userService.findById(1L)).thenReturn(user);
        
        mockMvc.perform(get("/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John"));
    }
}

Сложный пример: Extension с аннотацией

import org.junit.jupiter.api.extension.ExtendWith;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

// Создаём собственную аннотацию
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(TransactionalExtension.class)
public @interface Transactional {
    // Это аннотация автоматически регистрирует расширение
}

// Само расширение
public class TransactionalExtension implements BeforeEachCallback, AfterEachCallback {
    
    private TransactionStatus transactionStatus;
    
    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        // Начинаем транзакцию
        ApplicationContext appContext = SpringExtension.getApplicationContext(context);
        PlatformTransactionManager txManager = 
            appContext.getBean(PlatformTransactionManager.class);
        
        transactionStatus = txManager.getTransaction(
            new DefaultTransactionDefinition()
        );
    }
    
    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        // Откатываем транзакцию
        ApplicationContext appContext = SpringExtension.getApplicationContext(context);
        PlatformTransactionManager txManager = 
            appContext.getBean(PlatformTransactionManager.class);
        
        txManager.rollback(transactionStatus);
    }
}

// Использование:
@SpringBootTest
@Transactional  // автоматически регистрирует расширение
class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void testCreateUser() {
        // Выполняется внутри транзакции, которая откатится после теста
        User user = userService.createUser("John");
        
        // В БД нет этого пользователя (транзакция откатана)
    }
}

Лучшие практики

1. Используйте встроенные расширения Spring

// ПРАВИЛЬНО: используй @SpringBootTest
@SpringBootTest
class MyTest { }

// НЕПРАВИЛЬНО: не регистрируй SpringExtension вручную
// @ExtendWith(SpringExtension.class)  // это делает @SpringBootTest

2. Держите Extensions простыми

// ХОРОШО: одна ответственность
public class DatabaseCleanupExtension implements BeforeEachCallback {
    public void beforeEach(ExtensionContext context) {
        // только чистка БД
    }
}

// ПЛОХО: слишком много ответственности
public class MegaExtension implements BeforeEachCallback, AfterEachCallback,
                                      ParameterResolver, TestInstancePreProcessor {
    // Делаем всё сразу - трудно поддерживать
}

3. Комбинируйте Extensions правильно

@SpringBootTest
@ExtendWith({DatabaseCleanupExtension.class, LoggingExtension.class})
class UserServiceTest {
    // Extensions выполняются по порядку
}

Заключение

Extensions в Spring Boot / JUnit 5:

  • Современная архитектура для расширения фреймворка
  • Заменили Rules из JUnit 4 более гибким механизмом
  • Встроены в Spring Boot для удобства тестирования
  • Мощный инструмент для реализации cross-cutting concerns в тестах (логирование, очистка БД, подготовка данных)
Что такое Extension в Spring Boot? | PrepBro