Быстрее, выше, сильнее: оптимизируем Spring-контекст для тестов

от автора

Всем привет! Я Юнес, Схема взаимодействия сервисов

Схема взаимодействия сервисов

Задача: протестировать функцию в сервисе достижений «рассказать о достижении в соцсетях». Фича не связана с данными пользователя и использует только интеграцию “Social Service”. Запускаем все тесты с тегом “share-feature” и… получаем org.postgresql.util.PSQLException!

Причина ошибки не так важна. Это может быть и опечатка в адресе базы, и заблокированный пользователь, и проблемы с доступом. Главное, что мы понимаем: несмотря на то, что запускаемые тесты не имеют никакого отношения к базе с пользователями, Spring инициализирует бин DataSource, который устанавливает соединение с базой. 

Лишние бины в контексте приводят не только к повышенному потреблению ресурсов и увеличению времени выполнения, но и к непредсказуемому поведению, проблемам с отладкой и нестабильности. Неприятностей можно избежать, если оптимизировать Spring-контекст, желательно — сохранив нервы и позитивный настрой в процессе. 

Контекст в Spring — это основной интерфейс, который управляет бинами, внедряет зависимости (пресловутый Dependency Injection), обеспечивает доступ к ресурсам и делает много других полезных вещей.

Оптимизация ради оптимизации — не лучшая идея. Вот пример признаков, которые могут быть симптомами проблем с неоптимальным контекстом:

  • Медленное выполнение тестов. Если с ростом количества тестов время выполнения растет непропорционально, то одним из источников проблемы может быть неоптимальная конфигурация или чрезмерное использование бинов.

  • Высокое потребление памяти. Если в коде нет рекурсий или утечек памяти, а тесты все равно требуют больше и больше памяти, возможно, дело в контексте.

  • Нестабильные тесты. Если тесты периодически падают с исключениями вроде BeanCreationException, NoSuchBeanDefinitionException, UnsatisfiedDependencyException и другими, указывающими на проблемы со Spring, нужно приступать к оптимизации. 

  • Жесткие требования к тестовому окружению. Если вы даже один тест не можете запустить без всех ресурсов, доступов, баз данных, иначе он падает с “Failed to load ApplicationContext”, это может быть признаком проблем с контекстом.

Конфигурация в управлении контекстом

Классы конфигурации в Spring сигнализируют о том, что являются источником бинов. В конфигурациях можно задать @Configuration @ComponentScan(basePackages = "com.example") // Предполагается, что 'com.example' это корневой пакет проекта public class AchievementConfig{ … } Стоит выбирать специфичный пакет для сканирования: @Configuration @ComponentScan(basePackages = "com.example.achievement.service") public class AchievementConfig{ … }

@Import — для регистрации конкретного класса конфигурации в Spring-контексте. Один из частых вариантов использования — импорт нужного класса конфигурации.

@EnabelAutoConfiguration — включает автоконфигурацию Spring контекста. Те, кто работает со Spring Boot, знакомы с понятием автоконфигурации, когда Spring на основе зависимостей и пропертей регистрирует бины, которые могут вам пригодиться. Автоконфигурация автоматически отключается при использовании сегментации. 

Поскольку Spring не всегда успешно угадывает, какие бины нам нужны, а также тратит время на сканирование зависимостей, лучше не использовать @EnabelAutoConfiguration для тестов. Может сложиться ситуация, что вы хотите использовать автоконфигурацию, но вам нужно отключить часть бинов. Тогда вы можете либо использовать @EnabelAutoConfiguration со списком отключенных конфигураций:

@EnableAutoConfiguration(exclude = {   DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class,      HibernateJpaAutoConfiguration.class})

Либо настроить application.properties/application.yml:

spring.autoconfigure.exclude= \  org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, \  org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, \  org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
Пример

Проблема

Контекст для атвотестов поднимался за 120 секунд. Если что-то шло не так, приходилось вносить правки в код и перезапускать тесты, что делало отладку тестов настоящим кошмаром…

Диагностика

Проблема заключалась в неоптимальной конфигурации автотестов. Каждый тест загружал весь Spring Boot контекст приложения, что включало множество ненужных бинов и конфигураций. Это избыточное сканирование и инициализация значительно увеличивали время выполнения тестов.

Внесенные изменения

  1. Заменили @SpringBootTest на @ContextConfiguration: Это позволило загружать только необходимую конфигурацию для тестов.

  2. Создали специализированный конфигурационный класс TestConfig, который включал только те компоненты, которые действительно нужны для тестирования сервиса достижений.

  3. Использовали @ComponentScan с указанием конкретного пакета: Это ограничило сканирование только необходимым пакетом, содержащим AchievementService.

Код до изменений

@RunWith(SpringRunner.class) @SpringBootTest public class AchievementServiceTest {      @Autowired     private AchievementService achievementService;      @Test     public void testAchievement() {         // Тестирование логики выдачи достижений     } }

Код после изменений

@RunWith(SpringRunner.class) @ContextConfiguration(classes = {TestConfig.class}) public class AchievementServiceTest {      @Autowired     private AchievementService achievementService;      @Test     public void testAchievement() {         // Тестирование логики выдачи достижений     } }  @Configuration @ComponentScan(basePackageClasses = AchievementService.class) public class TestConfig {     // Конфигурация тестов, включающая только необходимые компоненты }

Результат

После внесения изменений, среднее время выполнения одного теста сократилось до 30 секунд, что в 4 раза быстрее по сравнению с предыдущим временем выполнения. Это значительно ускорило процесс отладки.

Конфигурация в Spring — ключевой инструмент для определения, какие бины будут зарегистрированы и будут контролироваться на протяжении всего времени выполнения кода. При оптимизации важно, какие бины конфигурация задействует и нет ли лишних, как настроено сканирование и есть ли автоконфигурация. Когда мы говорим про оптимизацию конфигурации, здесь минимум усилий может дать отличный результат, если ранее не проводился рефакторинг. 

Сегментация 

Сегментация, или test slicing, — это механизм Spring Boot, который позволяет разделить контекст на слои, чтобы тестировать их в изоляции друг от друга. В тестовом классе нужно указать аннотацию сегмента, и Spring инициализирует ограниченный контекст, который включает бины, относящиеся к данному сегменту. Доступны аннотации:

@SpringBootTest — позволяет тестировать все сегменты приложения совместно, и нет исключений по загружаемым бинам.

@WebMvcTest — для тестов слоя Model-View-Controller. Включает бины, связанные с Controller.

@WebFluxTest — включает бины с Controller, но только для reactive-контекста.

@RestClientTest — для тестирования Rest-взаимодействия. Автоматически создает для тестов мок rest-сервиса.

@WebServiceClientTest, @WebServiceServerTest — для тестирования SOAP клиента или сервера.

@JsonTest — тесты, завязанные только на Json-сериализацию. Например, JsonComponent.

@DataJpaTest — для тестов, которые используют только JPA-компоненты. Задействует транзакции в тестах, откатывает действия после прогона каждого теста и задействует готовую среду, в которой используется база данных in-memory.

@JdbcTest — аналогично @DataJpaTest позволяет использовать транзакции, только для JDBC-компонент.

@JooqTest, @DataElasticsearchTest, @DataRedisTest, @DataNeo4jTest, @DataCassandraTest, @DataCouchbaseTest, @DataMongoTest, @DataR2dbcTest — для тестов, которые нацелены на работу с конкретной базой или системой управления БД.

@GraphQlTest — для тестов, которые выполняют GraphQL-запросы.

@DataLdapTest — для тестов, связанных с LDAP-протоколом.

Можно создать свою аннотацию, чтобы выделить нужный тип бинов, которые будут обязательно присутствовать в контексте вашего теста.

Пример

Проблема

Автотесты для сервиса достижений выполнялись долго, и тесты не были изолированы друг от друга, возникали ошибки связанные с БД, даже когда запускались тесты контроллера. Из-за этого время выполнения тестов увеличивалось, а также возникали проблемы с повторяемостью результатов тестирования.

Диагностика

Тесты использовали общую конфигурацию, загружали весь контекст Spring Boot приложения, что приводило к длительному времени инициализации и отсутствию изоляции между тестами.

Внесенные изменения

  1. Использовали @WebMvcTest: Это позволило тестировать только веб-слой, не загружая весь контекст приложения.

  2. Использовали @DataJpaTest: Это позволило тестировать только слой данных, не загружая другие части приложения.

  3. Использовали специализированные конфигурации для различных типов тестов, чтобы обеспечить их изоляцию и ускорить выполнение.

Код до изменений

@RunWith(SpringRunner.class) @SpringBootTest public class AchievementServiceIntegrationTest {      @Autowired     private MockMvc mockMvc;      @Test     public void testGetAchievement() throws Exception {         mockMvc.perform(get("/achievements/1"))                .andExpect(status().isOk())                .andExpect(content().contentType(MediaType.APPLICATION_JSON))                .andExpect(jsonPath("$.id").value(1));     }      @Autowired     private AchievementRepository achievementRepository;      @Test     public void testFindAchievementById() {         Achievement achievement = achievementRepository.findById(1L).orElse(null);         assertNotNull(achievement);         assertEquals(1L, achievement.getId().longValue());     } } 

Код после изменений

Web Layer Test

@RunWith(SpringRunner.class) @WebMvcTest(AchievementController.class) public class AchievementControllerTest {      @Autowired     private MockMvc mockMvc;      @Test     public void testGetAchievement() throws Exception {         mockMvc.perform(get("/achievements/1"))                .andExpect(status().isOk())                .andExpect(content().contentType(MediaType.APPLICATION_JSON))                .andExpect(jsonPath("$.id").value(1));     } } 

Data Layer Test

@RunWith(SpringRunner.class) @DataJpaTest public class AchievementRepositoryTest {      @Autowired     private AchievementRepository achievementRepository;      @Test     public void testFindAchievementById() {         Achievement achievement = achievementRepository.findById(1L).orElse(null);         assertNotNull(achievement);         assertEquals(1L, achievement.getId().longValue());     } } 

Результат

После внесения изменений, проблем с падением тестов стало значительно меньше благодаря изоляции. Количество flaky тестов уменьшилось на 20%. Скорость поднятия контеста также уменьшилась из-за того, что меньше бинов стало инициализироваться, но незначительно из-за того, что запускаемых контекстов стало больше.

Явными преимуществами сегментации являются простота и удобство использования. Набор бинов уже предопределен, автоконфигурация сделает все за вас. Слои изолируются от остального контекста, что повышает контролируемость тестирования. Но аннотации недостаточно гибкие, если нужен детальный контроль над загружаемыми бинами. 

Не все тесты нуждаются в Spring-контексте. Можно оставить часть тестов без него, например юнит-тесты.

Условия инициализации бина

Spring предоставляет набор аннотаций для указания условий инициализации бина. Если условие выполняется, бин может быть зарегистрирован в контексте. Вот список условий, которые наиболее применимы для тестовых конфигураций и других бинов:

@ConditionalOnBean — если указанный бин есть в контексте.

@ConditionalOnClass — если указанный класс есть в classpath.

@ConditionalOnMissingBean — если бина нет в контексте.

@ConditionalOnMissingClass — если класса нет в classpath.

@ConditionalOnProperty — проверяет значение проперти в application.yml или application.properties.

@ConditionalOnResource — проверяет наличие файла в classpath. 

@ConditionalOnExpression — если логическое выражение возвращает true, бин инициализируется. Для составления выражений используется Spring Expression Language.

@ConditionalOnJava — бин инициализируется, если версия Java входит в перечень разрешенных.

Для одного бина можно использовать несколько условий. Я перечислил не все аннотации-условия, а наиболее подходящие для использования с тестами. Список всех актуальных аннотаций можно посмотреть на GitHub.

Создадим свою аннотацию-условие:

@Target({ElementType.TYPE, ElementType.METHOD})  @Retention(RetentionPolicy.RUNTIME)  @Conditional(OnDatabaseTypeCondition.class)  public @interface ConditionalOnDatabaseType {  DatabaseType value();  }

Реализуем интерфейс Condition:

public class OnDatabaseTypeCondition implements Condition {  @Override  public boolean matches(@NotNull ConditionContext context, AnnotatedTypeMetadata metadata) {      Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnDatabaseType.class.getName());      if (attributes != null) {          DatabaseType requiredDatabaseType = (DatabaseType) attributes.get("value");          String configuredDriverClassName = context.getEnvironment().getProperty("spring.datasource.driverClassName");          DatabaseType configuredDatabaseType = DatabaseType.fromDriverClassName(configuredDriverClassName);          return requiredDatabaseType.equals(configuredDatabaseType);      }      else {          return false;      }  }  }
Пример

Проблема

У нас возникла проблема с длительным временем загрузки контекста Spring в тестах сервиса достижений. Есть несколько тяжеловесных бинов, которые нужны в небольшом количестве тестов, но они инициализируются всегда, что занимает +10-15 секунд к времени инициализации контекста.

Диагностика

Мы выделили несколько сценариев, когда мы точно используем эти бины. Изучив все варианты оптимизации, мы обнаружили, что использование условий удобнее всего, т.к. нам нужно точечное изменение, создание новой конфигурации в данном случае не оправдано.

Внесенные изменения

  1. Создание кастомного условия: Было создано кастомное условие OnTestPropertyCondition, которое проверяет наличие определённого свойства в конфигурации.

  2. Аннотация бинов: Мы использовали аннотацию @Conditional для бинов, которые не всегда нужны, и настроили их загрузку в зависимости от свойства test.condition.enabled.

  3. Настройка свойств: В тестах, где нужна указанная зависимость, было добавлено свойство test.condition.enabled=true, чтобы активировать необходимые бины только для нужных тестов.

Код до изменений

@Configuration public class AchievementServiceTestConfig {      @Bean     public AchievementService achievementService() {         return new AchievementService();     }      // Другие бины }

Код после изменений

Кастомное условие

public class OnTestPropertyCondition implements Condition {      @Override     public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {         String property = context.getEnvironment().getProperty("test.condition.enabled");         return Boolean.parseBoolean(property);     } }

Конфигурация

@Configuration public class AchievementServiceTestConfig {      @Bean     @Conditional(OnTestPropertyCondition.class)     public AchievementService achievementService() {         return new AchievementService();     }      // Другие бины }

Тест

@SpringBootTest @TestPropertySource(properties = {"test.condition.enabled=true"}) public class AchievementServiceTest {      @Autowired     private AchievementService achievementService;      @Test     public void testAchievementService() {         assertNotNull(achievementService);         // дополнительные проверки     } }

Результат

После оптимизации тесты без тяжелых бинов стали запускаться быстрее, плюс мы добились лучшей изоляции тестов.

Вариантов применения в тестах аннотаций-условий — масса. Можно использовать их для инициализации конфигурации, если @TestConfiguration public class TestConfig {     @Bean     @Primary     public HeavyService heavyServiceMock() {         return Mockito.mock(HeavyService.class);     } }

Профили

В Spring есть возможность привязать бины к различным окружениям. Например, мы можем создать два класса для настройки соединений с базой данных. Для тестирования используем базу H2, а для прода — Postgresql. Можно привязывать бины к нужному нам окружению и адаптировать к разным условиям.

@Profile — аннотация указывает на профили, в которых бин должен или не должен быть инициализирован.

@ActiveProfile — аннотация для тестов, которая говорит Spring активировать указанные профили при запуске теста. Это нужно для теста бинов, которые инициализируются только при определенном профиле. Можно тестировать, одновременно задействуя несколько профилей. 

Профиль нужен для определения поведения в зависимости от окружения. Перед созданием нового профиля определите список окружений, в которых будут запущены тесты. Настройте классы конфигурации под каждое из них, используя аннотацию Profile для указания окружения. Так разграничится, какие бины нужны в текущем окружении, а какие можно не инициализировать и не помещать в контекст. Это позволяет экономить время и ресурсы.

Профилями не стоит злоупотреблять — например, создавать профили под каждую базу: dev-db-h2, dev-db-mysql… Дело в том, что при запуске теста с новым профилем Spring создает новый контекст. Поэтому злоупотребление аннотациями ActiveProfile может замедлить выполнение тестов.

Пример

Проблема

Отсутствие разбивки бинов по окружениям привело к необходимости иметь доступ к базам данных на qa и dev контурах одновременно для стендов. Это усложняет управление конфигурациями и увеличивает риск ошибок. Также это создаёт проблему с безопасностью.

Диагностика

Все бины и конфигурации загружаются независимо от окружения, что приводит к избыточным подключениям и конфигурациям, которые не нужны для конкретного окружения.

Внесенные изменения

  1. Создание профилей для разных окружений (dev и qa): Было определно два Spring профиля (qa и dev) для управления конфигурацией бинов в зависимости от окружения.

  2. Определение специфичных конфигураций для каждого окружения: Разделение конфигурационных файлов и бинов для dev и qa окружений.

Код до изменений

@Configuration public class DatabaseConfig {      @Autowired     private DevDatabaseProperties devDatabaseProperties;          @Autowired     private QaDatabaseProperties qaDatabaseProperties;          @Bean     public DataSource qaDataSource() {         return DataSourceBuilder.create()                 .url(qaDatabaseProperties.getUrl())                 .username(qaDatabaseProperties.getUsername())                 .password(qaDatabaseProperties.getPassword())                 .driverClassName(qaDatabaseProperties.getDriverClassName())                 .build();     }      @Bean     public DataSource devDataSource() {         return DataSourceBuilder.create()                 .url(devDatabaseProperties.getUrl())                 .username(devDatabaseProperties.getUsername())                 .password(devDatabaseProperties.getPassword())                 .driverClassName(devDatabaseProperties.getDriverClassName())                 .build();     } }

application.yml

dev:   datasource:     url: jdbc:h2:mem:devdb     driver-class-name: org.h2.Driver     username: ${DB_USERNAME}     password: ${DB_PASSWORD} qa:   datasource:     url: jdbc:h2:mem:qadb     driver-class-name: org.h2.Driver     username: ${DB_USERNAME}     password: ${DB_PASSWORD}

Тест

@RunWith(SpringRunner.class) @SpringBootTest public class AchievementServiceDevTest {      @Autowired     private AchievementDevService achievementService;      @Test     public void testAchievement() {         // Тестирование логики выдачи достижений на dev окружении     } }  @RunWith(SpringRunner.class) @SpringBootTest public class AchievementServiceQaTest {      @Autowired     private AchievementQaService achievementService;      @Test     public void testAchievement() {         // Тестирование логики выдачи достижений на dev окружении     } }

Код после изменений

@Configuration @Profile("dev") public class DevDatabaseConfig {      @Autowired     private DatabaseProperties properties;           @Bean     public DataSource dataSource() {         return DataSourceBuilder.create()                 .url(properties.getUrl())                 .username(properties.getUsername())                 .password(properties.getPassword())                 .driverClassName(properties.getDriverClassName())                 .build();     }      }

application-dev.yml

spring:   datasource:     url: jdbc:h2:mem:devdb     driver-class-name: org.h2.Driver     username: ${DB_USERNAME}     password: ${DB_PASSWORD}

application-qa.yml

spring:   datasource:     url: jdbc:h2:mem:qadb     driver-class-name: org.h2.Driver     username: ${DB_USERNAME}     password: ${DB_PASSWORD}

Тест

@RunWith(SpringRunner.class) @SpringBootTest @ActiveProfiles("dev") public class AchievementServiceDevTest {      @Autowired     private AchievementService achievementService;      @Test     public void testAchievementDev() {         // Тестирование логики выдачи достижений на dev окружении     } }  @RunWith(SpringRunner.class) @SpringBootTest @ActiveProfiles("qa") public class AchievementServiceQaTest {      @Autowired     private AchievementService achievementService;      @Test     public void testAchievementQa() {         // Тестирование логики выдачи достижений на qa окружении     } } 

Результат

Данные изменения избавили команду от многих проблем. Лучшая изоляция позволяет не обращать внимание на доступность БД из другого контура. Бины, которые нужны в qa окружении не инициализируются в dev окружении и наоборот, что экономит немного ресурсов и времени.

Главная задача профилей — обеспечить гибкую настройку контекста в зависимости от окружения. А еще они положительно влияют на изолированность тестов и производительность, поскольку в тестовой среде инициализируются только бины и конфигурации, нужные нам для тестирования. 

Мы стараемся избегать операций создания или пересоздания контекста, потому что Spring-контекст выполняет большое количество различных операций при запуске. Он проходит пять этапов инициализации, во время которых парсит конфигурацию, сканирует пакеты, регистрирует и создает бины, настраивает их. Все это требует ресурсов и времени. И чем больше бинов используется, тем дольше это происходит.

Дополнительная настройка контекста

Вот список аннотаций, которые влияют на работу контекста, но не подошли под остальные категории.

@Lazy — аннотация информирует Spring о том, что для данного бина нужна ленивая инициализация, то есть Spring инициализирует его только тогда, когда он будет вызван в коде. В тестах это помогает ускорить инициализацию контекста и снизить количество потребляемых ресурсов. Ею стоит помечать тяжелые бины, а также редко используемые компоненты. 

В отличие от бинов с другим профилем или бинов с невыполненным условием инициализации, ленивый бин присутствует в контексте и не вызывает UnsatisfiedDependencyException при попытке обращения к нему.

@DirtiesContext — информирует Spring о том, что контекст требуется перезапустить до или после выполнения указанного метода или класса. Она используется в тестах, чтобы возвращать исходное состояние бинам, очищать кэш, пересоздавать соединение с базой данных и так далее. Проблема в том, что пересоздание контекста — ресурсоемкая операция, которая @Service public class AchievementService { @Cacheable(value = "achievements", key = "#achievementId") public Achievement getAchievementById(Long achievementId) {     // код метода } @CachePut(value = "achievements", key = "#achievement.id") public Achievement updateAchievement(Achievement achievement) {     // код метода     return achievement; } @CacheEvict(value = "achievements", key = "#achievementId") public void deleteAchievement(Long achievementId) {     // код метода } }

Стало так:

@Service  @CacheConfig(cacheNames = "achievements")  public class AchievementService {  @Cacheable(key = "#achievementId")  public Achievement getAchievementById(Long achievementId) {      // код метода  }  @CachePut(key = "#achievement.id")  public Achievement updateAchievement(Achievement achievement) {      // код метода      return achievement;  }  @CacheEvict(key = "#achievementId")  public void deleteAchievement(Long achievementId) {      // код метода  }  }  @Caching — аннотация, которая позволяет указать несколько действий с кэшем для одного метода:  @Caching(      evict = { @CacheEvict("achievements"), @CacheEvict(value = "achievementIds", allEntries = true) },      put = { @CachePut(value = "achievements", key = "#achievement.id") }  )  public Achievement saveAchievement(Achievement achievement) { /* код метода */ }

В Spring кэшируются не только повтояемые и ресурсоёмких операции, но и контексты. Если для первого нужно явно указывать аннотации вроде Cachable, то с контекстами немного сложнее. Как только Spring загружает ApplicationContext (или WebApplicationContext) для теста, этот контекст кэшируется и повторно используется для всех последующих тестов, которые объявляют ту же уникальную конфигурацию контекста в том же наборе тестов. Как упоминалось ранее, аннотации вроде MockBean и SpyBean меняют конфигурацию контекста, что не позволяет её переиспользовать. Также любое изменение метаданных конфигурации (активные профили, подключенные классы конфигурации, родительский контекст и пр.) создаёт новый контекст, а не переиспользует старый. Поэтому, где это возможно, стоит не использовать аннотации, создающие уникальные контексты и искать баланс между изоляцией и модульностью и производительностью.

За время работы со Spring я вывел для себя два момента, как не сломать механизм кэширования и обернуть его в свою пользу:

  1. Запускать тесты, которые используют одинаковый контекст, в одном процессе. Контекст хранится в статической переменной в JVM, поэтому кэширование фактически не будет работать при запуске тестов в разных процессах. В Spring сводят кэширование к нулю Fork Mode в Maven или maxParallelForks в Gradle, которые запускают несколько процессов параллельно.

  1. Кэширование требует ресурсов, не стоит использовать его для всего подряд, это сделает только хуже. Не кэширую данные в тестах, если они часто меняются, зависят от контекста, узкоспециализированные или часто меняются. А еще слишком дорого кэшировать примитивы и легковычисляемые данные.

Кэширование полезно тогда, когда испробовано большинство решений по оптимизации, но нужно выжать еще из кода. При неправильном использовании кэширования могут начать падать тесты, потому что данные, получаемые из кэша, устарели или некорректны.

Анализируем результат

Перед оптимизацией контекста стоит понять его текущее состояние. Это будет точка, от которой будем отталкиваться. Нам важны следующие метрики.

Время инициализации контекста

Что означает: сколько времени нужно Spring, чтобы выполнить полную инициализацию контекста, начиная от парсинга конфигураций и заканчивая настройкой созданных бинов.

Зачем нужна: сведение к минимуму времени запуска ускоряет выполнение тестов, способствуя ускорению циклов разработки и обратной связи.

Чем измерять:
— Брать замеры от Spring. Spring при старте замеряет время старта контекста и выводит в виде сообщения “Started ExampleTest in 4.822 seconds (JVM running for 5.224)”. Нужно только установить уровень логирования не ниже INFO.

— Отлавливать события старта и завершения инициализации Spring-контекста с помощью ApplicationListener и считать разницу.

Потребление памяти

Что означает: количество оперативной памяти, которое используется во время выполнения кода.

Зачем нужна: для предотвращения проблем с памятью при запуске в окружении с ограниченными ресурсами.

Чем измерять: профилировщик — VisualVM, JProfiler и так далее.

Сканируемые бины для тестов

Что означает: какие бины Spring подтягивает для тестов.

Зачем нужна: для правильной конфигурации ComponentScan — чтобы не загружать ненужные бины.

Чем измерять: Spring выводит информацию о работе ComponenScan в логи, нужно выставить уровень логирования DEBUG и искать в логах информацию о ClassPathBeanDefinitionScanner через Ctrl+F.

В DEBUG-логах можно найти информацию о работе автоконфигурации, она выводится в CONDITIONS EVALUATION REPORT. Помимо этого, можно понаблюдать за работой кэша Spring (если вы кэшируете данные), включив логирование в application.properties: logging.level.org.springframework.test.context.cache=DEBUG.

После сбора метрик можно приступать к рефакторингу. После рефакторинга нужно повторно собрать метрики и сравнить. Также стоит повторно запустить тесты и убедиться, что изменения не повлияли на результаты — по крайней мере, в худшую сторону.

Выводы

Оптимизация контекста Spring для тестов заключается не только в повышении скорости выполнения тестов или экономии ресурсов. Речь идет также об обеспечении надежности, удобстве обслуживания и масштабируемости процесса тестирования, а также увеличении изолированности тестов. Надеюсь, приведенные в статье рекомендации помогут кому-нибудь улучшить тесты и сделать процесс тестирования эффективнее и стабильнее. Если вы знаете способы оптимизации контекста, не приведенные в статье, — буду рад, если поделитесь в комментариях 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/816051/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *