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

Как при наличии множества тестов, требующих контекста, распределить их по классам для однократного поднятия контекста

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 ускорение!)

Ключевые принципы:

  1. @SpringBootTest один раз на базовом классе
  2. Наследование для всех специфичных тестов
  3. @Transactional для откатов (контекст не перезагружается)
  4. Избегай @DirtiesContext если можно
  5. Группируй логически — User, Order, Payment в разных классах
  6. Используй профили — test, integration-test отдельно

Итог: правильная иерархия тестовых классов может ускорить сюиты в 10-20 раз.