← Назад к вопросам
Как при наличии множества тестов, требующих контекста, распределить их по классам для однократного поднятия контекста
2.0 Middle🔥 111 комментариев
#Spring Framework#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Оптимизация Spring контекста в тестах
Это критичная задача — каждое поднятие контекста может добавлять несколько секунд к тестам. С множеством тестов это становится огромной проблемой.
Проблема: Множественное поднятие контекста
// ❌ ПЛОХО — контекст поднимается для КАЖДОГО класса
@SpringBootTest
public class UserServiceTests {
// Контекст 1
@Test
public void testCreateUser() { ... }
}
@SpringBootTest
public class OrderServiceTests {
// Контекст 2 (новый, тот же конфиг)
@Test
public void testCreateOrder() { ... }
}
@SpringBootTest
public class PaymentServiceTests {
// Контекст 3 (новый, тот же конфиг)
@Test
public void testProcessPayment() { ... }
}
// Время: 3 секунды на контекст × 3 = 9 секунд просто на lift-up
Решение 1: Базовый тестовый класс (Abstract Test Class)
Один контекст на всех:
// Базовый класс с контекстом
@SpringBootTest
public abstract class BaseServiceTest {
@Autowired
protected UserRepository userRepository;
@Autowired
protected OrderRepository orderRepository;
@Autowired
protected PaymentService paymentService;
// Утилиты для всех тестов
protected User createTestUser(String email) {
User user = new User();
user.setEmail(email);
return userRepository.save(user);
}
protected Order createTestOrder(User user) {
Order order = new Order();
order.setUser(user);
return orderRepository.save(order);
}
}
// ✅ Разные тестовые классы наследуют базовый класс
public class UserServiceTests extends BaseServiceTest {
@Test
public void testCreateUser() {
User user = createTestUser("test@example.com");
assertNotNull(user.getId());
}
@Test
public void testFindUser() {
User user = createTestUser("user@example.com");
User found = userRepository.findByEmail("user@example.com");
assertEquals(user.getId(), found.getId());
}
}
public class OrderServiceTests extends BaseServiceTest {
@Test
public void testCreateOrder() {
User user = createTestUser("order@example.com");
Order order = createTestOrder(user);
assertNotNull(order.getId());
}
}
public class PaymentServiceTests extends BaseServiceTest {
@Test
public void testProcessPayment() {
User user = createTestUser("payment@example.com");
Order order = createTestOrder(user);
paymentService.process(order);
assertEquals("PAID", order.getStatus());
}
}
// ✅ Результат: Один контекст для всех трёх классов
// Время: 3 секунды (вместо 9 секунд)
Решение 2: Группировка по функциональности (Suite)
Для большого проекта группируй тесты логически:
// Папка: tests/unit/user/
@SpringBootTest
public abstract class UserTestSuite {
@Autowired
protected UserRepository repo;
@Autowired
protected UserService service;
}
public class UserCreationTests extends UserTestSuite {
@Test
public void testCreateWithEmail() { ... }
@Test
public void testCreateWithPhone() { ... }
@Test
public void testValidation() { ... }
}
public class UserUpdateTests extends UserTestSuite {
@Test
public void testUpdateEmail() { ... }
@Test
public void testUpdatePassword() { ... }
}
public class UserDeletionTests extends UserTestSuite {
@Test
public void testDelete() { ... }
@Test
public void testDeleteCascade() { ... }
}
// Папка: tests/unit/order/
@SpringBootTest
public abstract class OrderTestSuite {
@Autowired
protected OrderRepository repo;
@Autowired
protected OrderService service;
}
public class OrderCreationTests extends OrderTestSuite { ... }
public class OrderPaymentTests extends OrderTestSuite { ... }
Решение 3: Shared контекст конфигурация
Для сложных конфигураций используй собственный контекст:
// shared/TestApplicationContext.java
@Configuration
public class TestApplicationContext {
@Bean
public DataSource testDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("test-data.sql")
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource ds) {
return new JdbcTemplate(ds);
}
}
// Все тесты используют одну конфигурацию
@SpringBootTest(classes = TestApplicationContext.class)
public abstract class BaseTest {
// Контекст поднимается один раз
}
public class UserTests extends BaseTest { ... }
public class OrderTests extends BaseTest { ... }
public class PaymentTests extends BaseTest { ... }
Решение 4: @DirtiesContext правильное использование
@SpringBootTest
public class UserServiceTests extends BaseServiceTest {
@Test
@Transactional
public void testCreateAndModify() {
// @Transactional откатывает данные после теста
// Контекст НЕ перезагружается
User user = new User();
user.setEmail("test@example.com");
userRepository.save(user);
}
// ❌ ПЛОХО — перезагружает контекст для следующего теста
@Test
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public void testWhichBreaksContext() {
// Используй только если действительно нужно
}
// ✅ ХОРОШО — перезагружает контекст один раз, после всех тестов
@Test
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
public void testConfigChange() {
// Это для тестов, меняющих конфиг Spring
}
}
Решение 5: Тестовые профили для разных контекстов
Если нужны разные конфигурации:
// application-test.properties
spring.jpa.database=h2
spring.h2.console.enabled=true
// application-integration-test.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/test_db
// Юнит-тесты — быстрые, в памяти
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@ActiveProfiles("test")
public class UserServiceUnitTests extends BaseServiceTest { ... }
// Интеграционные тесты — на реальной БД
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("integration-test")
public class UserServiceIntegrationTests extends BaseServiceTest { ... }
// Даже здесь один контекст на профиль!
Решение 6: Структура проекта для оптимизации
tests/
├── unit/
│ ├── base/
│ │ ├── BaseServiceTest.java ← Один контекст
│ │ └── TestConfig.java
│ ├── user/
│ │ ├── UserCreationTest.java ← Наследует Base
│ │ ├── UserUpdateTest.java ← Наследует Base
│ │ └── UserDeletionTest.java ← Наследует Base
│ ├── order/
│ │ ├── OrderCreationTest.java ← Наследует Base
│ │ └── OrderPaymentTest.java ← Наследует Base
│ └── payment/
│ └── PaymentProcessTest.java ← Наследует Base
├── integration/
│ ├── base/
│ │ └── BaseIntegrationTest.java ← Другой контекст (если нужен)
│ └── workflow/
│ └── CheckoutWorkflowTest.java
└── e2e/
└── UserJourneyTest.java
Решение 7: Оптимизированный BaseTest класс
@SpringBootTest(
classes = TestApplication.class,
webEnvironment = WebEnvironment.NONE, // Не поднимаем полный сервер
properties = {"spring.jpa.show-sql=false"} // Отключаем лишние логи
)
@TestExecutionListeners({
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class
})
public abstract class BaseServiceTest {
// Общие beans
@Autowired
protected UserRepository userRepository;
@Autowired
protected OrderRepository orderRepository;
@Autowired
protected PaymentService paymentService;
// Утилиты для setup/teardown
@BeforeEach
public void beforeEach() {
// Очищаем данные перед каждым тестом
userRepository.deleteAll();
orderRepository.deleteAll();
}
// Хелперы для создания тестовых объектов
protected User newUser(String email) {
User u = new User();
u.setEmail(email);
return u;
}
protected void assertUserExists(String email) {
assertTrue(userRepository.existsByEmail(email));
}
}
Бенчмарк улучшений:
До оптимизации (50 тестов, 30 классов):
├── UserServiceTests: 3s (контекст)
├── UserRepositoryTests: 3s (новый контекст)
├── OrderServiceTests: 3s (новый контекст)
├── ... 27 классов × 3s = 81s
└── Всего: ~100 секунд (большая часть — поднятие контекстов)
После оптимизации (одна иерархия классов):
├── Базовый контекст: 3s
├── UserTests (все 10 методов): 0.5s
├── OrderTests (все 15 методов): 0.5s
├── PaymentTests (все 25 методов): 0.5s
└── Всего: ~5 секунд (20x ускорение!)
Ключевые принципы:
- @SpringBootTest один раз на базовом классе
- Наследование для всех специфичных тестов
- @Transactional для откатов (контекст не перезагружается)
- Избегай @DirtiesContext если можно
- Группируй логически — User, Order, Payment в разных классах
- Используй профили — test, integration-test отдельно
Итог: правильная иерархия тестовых классов может ускорить сюиты в 10-20 раз.