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

Сколько контекстов Spring Boot у двух классов с аннотацией SpringBootTest?

2.7 Senior🔥 121 комментариев
#Spring Boot и Spring Data#Тестирование

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

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

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

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 создаст ОДИ контекст и переиспользует его
// для обоих тестовых классов!

Причины переиспользования контекста:

  1. Идентичная конфигурация — оба класса используют @SpringBootTest с одинаковыми параметрами
  2. Один и тот же Spring Boot класс приложения
  3. 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
Разные @ActiveProfiles2Разные ключи кэша
Разные @TestPropertySource2Разные ключи кэша
Одна конфигурация + @DirtiesContext2+Контекст очищается
Разные classes параметры2Разные ключи кэша
Разные веб-окружения (web=MOCK vs RANDOM_PORT)2Разные конфигурации

Производительность

Переиспользование контекста существенно ускоряет тесты:

Первый запуск (создание контекста): 5-10 секунд
Переиспользование контекста: <100ms

С 10 тестовыми классами и одним контекстом: ~10 секунд
С 10 тестовыми классами и 10 разными контекстами: ~100 секунд!

Best Practices

  1. Переиспользуйте контексты — избегайте @DirtiesContext без причины
  2. Используйте одинаковые конфигурации для всех интеграционных тестов
  3. Отделяйте unit-тесты от интеграционных — unit-тесты не нужны @SpringBootTest
  4. Логируйте создание контекстов при отладке проблем с тестами
  5. Мониторьте время выполнения — долгие тесты могут указывать на ненужное пересоздание контекстов

Итог

Стандартный ответ: Два класса с идентичной @SpringBootTest конфигурацией разделяют один ApplicationContext, потому что Spring кэширует и переиспользует контексты с одинаковыми ключами конфигурации. Это критически важная оптимизация, которая ускоряет выполнение интеграционных тестов и демонстрирует понимание внутреннего устройства Spring Test Framework.