Сколько контекстов Spring Boot у двух классов с аннотацией SpringBootTest?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Spring Boot Test Контексты: кэширование и переиспользование
Это отличный вопрос, который показывает глубокое понимание того, как работает Spring Boot тестирование! При наличии двух тестовых классов с @SpringBootTest количество контекстов зависит от конфигурации и кэширования.
Краткий ответ: В большинстве случаев это один контекст для обоих классов, потому что Spring кэширует контексты и переиспользует их, если они идентичны. Но есть важные нюансы.
Понимание Spring Test Context Caching
Spring TestContext Framework кэширует контексты в памяти, чтобы ускорить выполнение тестов:
// Тестовый класс 1
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testCreateUser() {
User user = userService.create("test@example.com");
assertNotNull(user);
}
}
// Тестовый класс 2
@SpringBootTest
public class ProductServiceTest {
@Autowired
private ProductService productService;
@Test
public void testGetProduct() {
Product product = productService.findById(1L);
assertNotNull(product);
}
}
// Результат: Spring создаст ОДИ контекст и переиспользует его
// для обоих тестовых классов!
Причины переиспользования контекста:
- Идентичная конфигурация — оба класса используют
@SpringBootTestс одинаковыми параметрами - Один и тот же Spring Boot класс приложения
- Spring Context Cache Key — ключ кэша зависит от конфигурации
Как Spring вычисляет ключ кэша контекста
// Spring использует MergedContextConfiguration для создания ключа
public class MergedContextConfiguration {
private final Class<?> testClass;
private final Class<?>[] classes; // @SpringBootTest(classes = ...)
private final String[] locations;
private final String[] activeProfiles;
private final PropertySourceProperties[] propertySourceProperties;
private final ContextLoader contextLoader;
// ... и другие параметры
// Если две конфигурации идентичны, контекст переиспользуется!
}
Сценарий 1: Один контекст (переиспользование)
// Тест 1
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testUser() {
assertNotNull(userService);
}
}
// Тест 2
@SpringBootTest
public class ProductServiceTest {
@Autowired
private ProductService productService;
@Test
public void testProduct() {
assertNotNull(productService);
}
}
// Результат:
// Контекстов: 1
// Оба теста используют один и тот же ApplicationContext
// Spring кэширует контекст и переиспользует его
Сценарий 2: Два контекста (разные конфигурации)
Вот когда Spring создаст два разных контекста:
// Тест 1: Базовая конфигурация
@SpringBootTest
public class UserServiceTest {
// ...
}
// Тест 2: С другой конфигурацией
@SpringBootTest(properties = {"app.feature.enabled=true"})
public class ProductServiceTest {
// ...
}
// Результат:
// Контекстов: 2
// Разные properties → разные ключи кэша → два контекста
Сценарий 3: Два контекста (разные active profiles)
// Тест 1: Development профиль
@SpringBootTest
@ActiveProfiles("dev")
public class UserServiceDevTest {
// ...
}
// Тест 2: Production профиль
@SpringBootTest
@ActiveProfiles("prod")
public class UserServiceProdTest {
// ...
}
// Результат:
// Контекстов: 2
// Разные active profiles → разные ключи кэша → два контекста
Как Spring определяет, когда переиспользовать контекст
// Упрощённый пример логики Spring
public class TestContextCache {
private final Map<MergedContextConfiguration, ApplicationContext> contextCache
= new ConcurrentHashMap<>();
public ApplicationContext getOrCreateContext(MergedContextConfiguration config) {
// 1. Вычисляем ключ кэша
String cacheKey = generateCacheKey(config);
// 2. Проверяем, есть ли контекст в кэше
ApplicationContext context = contextCache.get(cacheKey);
if (context != null) {
System.out.println("Context cache hit! Переиспользуем контекст.");
return context;
}
// 3. Создаём новый контекст
System.out.println("Context cache miss! Создаём новый контекст.");
context = createApplicationContext(config);
contextCache.put(cacheKey, context);
return context;
}
private String generateCacheKey(MergedContextConfiguration config) {
// Ключ зависит от:
// - Test class
// - @SpringBootTest параметры
// - @ActiveProfiles
// - @TestPropertySource
// - и других конфигурационных аннотаций
return config.toString();
}
}
Практический пример: демонстрация переиспользования
// Логирование для видения создания контекстов
@SpringBootTest
public class UserServiceTest {
private static final Logger logger = LoggerFactory.getLogger(UserServiceTest.class);
@Autowired
private UserService userService;
@BeforeClass
public static void beforeClassSetUp() {
logger.info("UserServiceTest: создание класса");
}
@Before
public void setUp() {
logger.info("UserServiceTest: setUp (контекст может быть переиспользуемым)");
}
@Test
public void testUser() {
logger.info("UserServiceTest: выполнение теста");
assertNotNull(userService);
}
}
@SpringBootTest // Та же конфигурация
public class ProductServiceTest {
private static final Logger logger = LoggerFactory.getLogger(ProductServiceTest.class);
@Autowired
private ProductService productService;
@BeforeClass
public static void beforeClassSetUp() {
logger.info("ProductServiceTest: создание класса");
}
@Before
public void setUp() {
logger.info("ProductServiceTest: setUp (переиспользуем контекст!)");
}
@Test
public void testProduct() {
logger.info("ProductServiceTest: выполнение теста");
assertNotNull(productService);
}
}
// Вывод консоли:
// INFO - UserServiceTest: создание класса
// INFO - UserServiceTest: setUp
// INFO - UserServiceTest: выполнение теста
// INFO - ProductServiceTest: создание класса
// INFO - ProductServiceTest: setUp (контекст переиспользуется!)
// INFO - ProductServiceTest: выполнение теста
// Видим: контекст был создан один раз перед UserServiceTest
// и переиспользуется для ProductServiceTest
Как отключить кэширование контекстов
Если нужен свежий контекст для каждого теста:
// Вариант 1: Использовать @DirtiesContext
@SpringBootTest
public class UserServiceTest {
@DirtiesContext // Контекст будет очищен после этого теста
@Test
public void testUser() {
// ...
}
}
// Вариант 2: Очищать контекст после каждого класса
@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class ProductServiceTest {
// Контекст будет очищен после всех тестов в этом классе
}
// Вариант 3: Очищать после каждого метода
@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class OrderServiceTest {
// Каждый тест получит свежий контекст
}
Таблица: когда Spring переиспользует контекст
| Сценарий | Контексты | Объяснение |
|---|---|---|
| Две конфигурации идентичны | 1 | Кэш hit |
Разные @ActiveProfiles | 2 | Разные ключи кэша |
Разные @TestPropertySource | 2 | Разные ключи кэша |
Одна конфигурация + @DirtiesContext | 2+ | Контекст очищается |
Разные classes параметры | 2 | Разные ключи кэша |
| Разные веб-окружения (web=MOCK vs RANDOM_PORT) | 2 | Разные конфигурации |
Производительность
Переиспользование контекста существенно ускоряет тесты:
Первый запуск (создание контекста): 5-10 секунд
Переиспользование контекста: <100ms
С 10 тестовыми классами и одним контекстом: ~10 секунд
С 10 тестовыми классами и 10 разными контекстами: ~100 секунд!
Best Practices
- Переиспользуйте контексты — избегайте
@DirtiesContextбез причины - Используйте одинаковые конфигурации для всех интеграционных тестов
- Отделяйте unit-тесты от интеграционных — unit-тесты не нужны @SpringBootTest
- Логируйте создание контекстов при отладке проблем с тестами
- Мониторьте время выполнения — долгие тесты могут указывать на ненужное пересоздание контекстов
Итог
Стандартный ответ: Два класса с идентичной @SpringBootTest конфигурацией разделяют один ApplicationContext, потому что Spring кэширует и переиспользует контексты с одинаковыми ключами конфигурации. Это критически важная оптимизация, которая ускоряет выполнение интеграционных тестов и демонстрирует понимание внутреннего устройства Spring Test Framework.