Рубрики
Без рубрики

Оптимизация весенних интеграционных тестов

Самоуверенное руководство по внедрению и оптимизации интеграционных тестов с помощью Spring.

Автор оригинала: José Carlos Valero Sánchez.

1. введение

В этой статье мы подробно обсудим интеграционные тесты с использованием Spring и способы их оптимизации.

Во-первых, мы кратко обсудим важность интеграционных тестов и их место в современном программном обеспечении, сосредоточив внимание на экосистеме Spring.

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

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

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

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

2. Интеграционные тесты

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

Чем больше мы упрощаем наш код с помощью модулей Spring (data, security, social…), тем больше потребность в интеграционных тестах. Это становится особенно верно, когда мы перемещаем биты и бобы нашей инфраструктуры в классы @Configuration .

Мы не должны “тестировать фреймворк”, но мы, безусловно, должны убедиться, что фреймворк настроен для удовлетворения наших потребностей.

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

  • Это более медленная скорость выполнения, что означает более медленные сборки
  • Кроме того, интеграционные тесты подразумевают более широкую область тестирования, которая в большинстве случаев не идеальна

Имея это в виду, мы попытаемся найти некоторые решения для смягчения вышеупомянутых проблем.

3. Тестирование веб-Приложений

Spring предлагает несколько вариантов для тестирования веб-приложений, и большинство разработчиков Spring знакомы с ними, это:

  • MockMvc : Издевается над API сервлетов, полезным для нереактивных веб-приложений
  • TestRestTemplate : Можно использовать, указывая на наше приложение, полезное для нереактивных веб-приложений, где издевательские сервлеты нежелательны
  • Веб-тестовый клиент : Это инструмент тестирования реактивных веб-приложений, как с поддельными запросами/ответами, так и с реальным сервером

Поскольку у нас уже есть статьи на эти темы, мы не будем тратить время на их обсуждение.

Не стесняйтесь взглянуть, если хотите копнуть глубже.

4. Оптимизация Времени Выполнения

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

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

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

Кроме того, интеграционные тесты по своей сути являются дорогостоящими. Запуск какой-либо персистентности, отправка запросов (даже если они никогда не покидают localhost ) или выполнение некоторых операций ввода-вывода просто требует времени.

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

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

  • Разумное использование профилей – как профили влияют на производительность
  • Пересмотр @MockBean – как издевательство влияет на производительность
  • Рефакторинг @MockBean – альтернативы для повышения производительности
  • Тщательно продумайте @ DirtiesContext – полезную, но опасную аннотацию и как ее не использовать
  • Использование тестовых срезов – классный инструмент, который может помочь или встать на нашем пути
  • Использование наследования классов – способ безопасной организации тестов
  • Государственное управление – передовая практика, позволяющая избежать ложных тестов
  • Рефакторинг в модульные тесты – лучший способ получить прочную и быструю сборку

Давайте начнем!

4.1. Разумное использование Профилей

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

По мере того как наши профили становятся богаче, возникает соблазн время от времени менять местами наши интеграционные тесты. Для этого есть удобные инструменты, такие как @ActiveProfiles . Однако каждый раз, когда мы выполняем тест с новым профилем, создается новый ApplicationContext .

Создание контекстов приложений может быть быстрым с помощью приложения vanilla spring boot, в котором ничего нет. Добавьте РУКУ и несколько модулей, и он быстро взлетит до 7+ секунд.

Добавьте кучу профилей и разбросайте их по нескольким тестам, и мы быстро получим сборку за 60 с лишним секунд (при условии, что мы запускаем тесты как часть нашей сборки – и мы должны это сделать).

Как только мы сталкиваемся с достаточно сложным приложением, исправить это становится непросто. Однако, если мы тщательно планируем заранее, становится тривиальным сохранить разумное время сборки.

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

  • Создайте совокупный профиль, т. е. test , включите в него все необходимые профили – придерживайтесь нашего тестового профиля везде
  • Разрабатывайте наши профили с учетом возможности тестирования. Если нам в конечном итоге придется переключать профили, возможно, есть лучший способ
  • Изложите наш тестовый профиль в централизованном месте – мы поговорим об этом позже
  • Избегайте тестирования всех комбинаций профилей. В качестве альтернативы мы могли бы использовать среду perfschema с набором тестов e2e, тестирующую приложение с помощью этого конкретного набора профилей

4.2. Проблемы с @MockBean

@@Mockbeat – довольно мощный инструмент.

Когда нам нужна какая-то весенняя магия, но мы хотим поиздеваться над определенным компонентом, @Mockbeat очень пригодится. Но за это приходится платить.

Каждый раз, когда @Mockbeat появляется в классе, кэш ApplicationContext помечается как грязный, поэтому бегун очистит кэш после завершения тестового класса. Что снова добавляет дополнительную кучу секунд к нашей сборке.

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

Мы можем подумать: зачем нам упорствовать, если все, что мы хотим проверить, – это наш слой ОТДЫХА? Это справедливое замечание, и всегда есть компромисс.

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

4.3. Рефакторинг @MockBean

В этом разделе мы попытаемся провести рефакторинг “медленного” теста с использованием @@Mockbeat чтобы заставить его повторно использовать кэшированные ApplicationContext .

Предположим, мы хотим протестировать СООБЩЕНИЕ, которое создает пользователя. Если бы мы издевались , используя @MockBean , мы могли бы просто проверить, что наша служба была вызвана с хорошо сериализованным пользователем.

Если мы правильно протестировали наш сервис, этого подхода должно быть достаточно:

class UsersControllerIntegrationTest : AbstractSpringIntegrationTest() {

    @Autowired
    lateinit var mvc: MockMvc
    
    @MockBean
    lateinit var userService: UserService

    @Test
    fun links() {
        mvc.perform(post("/users")
          .contentType(MediaType.APPLICATION_JSON)
          .content("""{ "name":"jose" }"""))
          .andExpect(status().isCreated)
        
        verify(userService).save("jose")
    }
}

interface UserService {
    fun save(name: String)
}

Однако мы хотим избежать @Mockbeat . Таким образом, мы в конечном итоге сохраним сущность (предполагая, что это то, что делает служба).

Самым наивным подходом здесь было бы проверить побочный эффект: после публикации мой пользователь находится в моей БД, в нашем примере это будет использовать JDBC.

Это, однако, нарушает границы тестирования:

@Test
fun links() {
    mvc.perform(post("/users")
      .contentType(MediaType.APPLICATION_JSON)
      .content("""{ "name":"jose" }"""))
      .andExpect(status().isCreated)

    assertThat(
      JdbcTestUtils.countRowsInTable(jdbcTemplate, "users"))
      .isOne()
}

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

Если мы реализуем наше приложение через HTTP, можем ли мы также утверждать результат через HTTP?

@Test
fun links() {
    mvc.perform(post("/users")
      .contentType(MediaType.APPLICATION_JSON)
      .content("""{ "name":"jose" }"""))
      .andExpect(status().isCreated)

    mvc.perform(get("/users/jose"))
      .andExpect(status().isOk)
}

Есть несколько преимуществ, если мы будем следовать последнему подходу:

  • Наш тест начнется быстрее (возможно, его выполнение может занять немного больше времени, но он должен окупиться)
  • Кроме того, наш тест не знает о побочных эффектах, не связанных с границами HTTP, т. е. DBS
  • Наконец, наш тест четко выражает цель системы: если вы опубликуете, вы сможете ПОЛУЧИТЬ пользователей

Конечно, это не всегда возможно по разным причинам:

  • У нас может не быть конечной точки “побочного эффекта”: здесь можно рассмотреть возможность создания “конечных точек тестирования”.
  • Сложность слишком высока, чтобы охватить все приложение: здесь можно рассмотреть срезы (мы поговорим о них позже)

4.4. Тщательно Продумать @DirtiesContext

Иногда нам может потребоваться изменить ApplicationContext в наших тестах. Для этого сценария @DirtiesContext предоставляет именно эту функциональность.

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

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

4.5. Использование тестовых срезов

Тестовые срезы-это функция загрузки Spring, представленная в версии 1.4. Идея довольно проста: Spring создаст сокращенный контекст приложения для определенного среза вашего приложения.

Кроме того, фреймворк позаботится о настройке самого минимума.

В Spring Boot есть разумное количество срезов, доступных из коробки, и мы также можем создать свои собственные:

  • @JsonTest: Регистрирует соответствующие компоненты JSON
  • @DataJpaTest : Регистрирует компоненты JPA, включая доступные ORM
  • @JdbcTest : Полезно для необработанных тестов JDBC, заботится об источнике данных и базах данных в памяти без излишеств ORM
  • @DataMongoTest : Пытается обеспечить установку тестирования mongo в памяти
  • @@WebMvcTest : фрагмент тестирования MockMvc без остальной части приложения
  • … (мы можем проверить источник , чтобы найти их все)

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

Однако, если наше приложение продолжает расти, оно также накапливается, поскольку создает один (небольшой) контекст приложения на срез.

4.6. Использование наследования классов

Использование одного класса Abstract Spring Integration Test в качестве родительского для всех наших интеграционных тестов-это простой, мощный и прагматичный способ ускорить сборку.

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

Мы могли бы установить там все требования к тестированию:

  • Весенний бегун – или, предпочтительно, правила, на случай, если позже нам понадобятся другие бегуны
  • профили – в идеале наш агрегат тест профиль
  • начальная конфигурация – настройка состояния нашего приложения

Давайте посмотрим на простой базовый класс, который заботится о предыдущих пунктах:

@SpringBootTest
@ActiveProfiles("test")
abstract class AbstractSpringIntegrationTest {

    @Rule
    @JvmField
    val springMethodRule = SpringMethodRule()

    companion object {
        @ClassRule
        @JvmField
        val SPRING_CLASS_RULE = SpringClassRule()
    }
}

4.7. Государственное управление

Важно помнить, откуда берется “единица измерения” в модульном тесте /. Проще говоря, это означает, что мы можем запустить один тест (или подмножество) в любой момент, получая согласованные результаты.

Следовательно, состояние должно быть чистым и известным до начала каждого теста.

Другими словами, результат теста должен быть согласованным независимо от того, выполняется ли он изолированно или вместе с другими тестами.

Эта идея точно так же применима к интеграционным тестам. Мы должны убедиться, что наше приложение имеет известное (и воспроизводимое) состояние, прежде чем начинать новый тест. Чем больше компонентов было использовано для ускорения работы (appcontext, DBS, очереди, файлы…), тем больше шансов получить загрязнение состояния.

Предполагая, что мы пошли ва-банк с наследованием классов, теперь у нас есть центральное место для управления состоянием.

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

В нашем примере мы предположим, что существует несколько репозиториев (из различных источников данных) и Wiremock сервер:

@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureWireMock(port = 8666)
@AutoConfigureMockMvc
abstract class AbstractSpringIntegrationTest {

    //... spring rules are configured here, skipped for clarity

    @Autowired
    protected lateinit var wireMockServer: WireMockServer

    @Autowired
    lateinit var jdbcTemplate: JdbcTemplate

    @Autowired
    lateinit var repos: Set>

    @Autowired
    lateinit var cacheManager: CacheManager

    @Before
    fun resetState() {
        cleanAllDatabases()
        cleanAllCaches()
        resetWiremockStatus()
    }

    fun cleanAllDatabases() {
        JdbcTestUtils.deleteFromTables(jdbcTemplate, "table1", "table2")
        jdbcTemplate.update("ALTER TABLE table1 ALTER COLUMN id RESTART WITH 1")
        repos.forEach { it.deleteAll() }
    }

    fun cleanAllCaches() {
        cacheManager.cacheNames
          .map { cacheManager.getCache(it) }
          .filterNotNull()
          .forEach { it.clear() }
    }

    fun resetWiremockStatus() {
        wireMockServer.resetAll()
        // set default requests if any
    }
}

4.8. Рефакторинг в модульные тесты

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

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

Возможной моделью здесь для успешного выполнения этой задачи может быть:

  • Определите интеграционные тесты, которые тестируют несколько сценариев основной бизнес-логики
  • Дублируйте набор и рефакторингуйте копию в модульные тесты – на этом этапе нам, возможно, потребуется также разбить производственный код, чтобы сделать его тестируемым
  • Получите все тесты зелеными
  • Оставьте образец счастливого пути, который достаточно примечателен в пакете интеграции – возможно, нам потребуется провести рефакторинг или присоединиться и изменить некоторые из них
  • Удалите оставшиеся интеграционные тесты

Майкл Перья охватывает множество методов для достижения этой цели и многое другое в эффективной работе с устаревшим кодом.

5. Резюме

В этой статье мы познакомились с интеграционными тестами с акцентом на Spring.

Во-первых, мы говорили о важности интеграционных тестов и о том, почему они особенно актуальны в весенних приложениях.

После этого мы обобщили некоторые инструменты, которые могут пригодиться для определенных типов интеграционных тестов в веб-приложениях.

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