Для чего нужно интеграционное тестирование?

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

Эта статья является конспектом книги «Принципы юнит-тестирования». Материал статьи посвящен интеграционным тестам.

Юнит-тесты прекрасно справляются с проверкой бизнес-логики, но проверять эту логику «в вакууме» недостаточно. Необходимо проверять, как разные ее части интегрируются друг с другом и внешними системами: базой данных, шиной сообщений и т. д.

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

Что такое интеграционный тест?

Юнит-тест удовлетворяет следующим трем требованиям:

  • проверяет правильность работы одной единицы поведения;

  • делает это быстро; 

  • и в изоляции от других тестов. 

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

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

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

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

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

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

Какие из внепроцессных зависимостей должны проверяться напрямую

Все внепроцессные зависимости делятся на две категории.

  • Управляемые зависимости (внепроцессные зависимости, находящиеся под вашим полным контролем): эти зависимости доступны только через ваше приложение; взаимодействия с ними не видны внешнему миру. Типичный пример — база данных.

  • Неуправляемые зависимости (внепроцессные зависимости, которые не находятся под вашим полным контролем) — результат взаимодействия с такими зависимостями виден извне. В качестве примеров можно привести сервер SMTP и шину сообщений.

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

Рис. 1 – Взаимодействия с зависимостями
Рис. 1 – Взаимодействия с зависимостями

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

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

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

База данных — не лучший механизм для интеграции между системами, потому что она связывает эти системы друг с другом и усложняет дальнейшую их разработку. Используйте это решение только в случае, если других вариантов нет. Правильнее осуществлять интеграцию через API (для синхронных взаимодействий) или шину сообщений (для асинхронных взаимодействий).

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

Рис. 2 – БД, доступная для внешних приложений
Рис. 2 – БД, доступная для внешних приложений

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

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

  • явное определение границ доменной модели (модели предметной области);

  • сокращение количества слоев в приложении;

  • устранение циклических зависимостей.

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

Сокращение количества слоев. Многие разработчики стремятся к абстрагированию и обобщению кода путем введения дополнительных уровней абстракции.

Рис. 3 – Типичное корпоративное приложение с несколькими слоями
Рис. 3 – Типичное корпоративное приложение с несколькими слоями

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

«Все проблемы в программировании можно решить путем добавления нового уровня абстракции (кроме проблемы наличия слишком большого количества уровней абстракции)».
Дэвид Дж. Уилер

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

Исключение циклических зависимостей. Циклическая зависимость возникает в том случае, если два или более класса прямо или косвенно зависят друг от друга. Типичный пример циклической зависимости — обратный вызов:

public class CheckOutService
{
  public void CheckOut(int orderId)
  {
    var service = new ReportGenerationService();
    service.GenerateReport(orderId, this);
    /* остальной код */
  }
}

public class ReportGenerationService
{
  public void GenerateReport(
    int orderId,
    CheckOutService checkOutService)
  {
  	/* вызывает checkOutService при завершении генерирования */
  }
}

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

Что же делать с циклическими зависимостями? Лучше всего совсем избавиться от них. Отрефакторить класс ReportGenerationService, чтобы он не зависел от CheckOutService и сделать так, чтобы ReportGenerationService возвращал результат работы в виде простого значения вместо вызова CheckOutService:

public class CheckOutService
{
  public void CheckOut(int orderId)
  {
    var service = new ReportGenerationService();
    Report report = service.GenerateReport(orderId);
    /* прочая работа */
  }
}

public class ReportGenerationService
{
  public Report GenerateReport(int orderId)
  {
  	/* ... */
  }
}

Использование нескольких секций действий в тестах

Как вы, возможно, помните из предыдущего конспекта «Анатомия юнит-тестов», наличие более одной секции подготовки, действий или проверки в тесте — плохой признак. Он указывает на то, что тест проверяет несколько единиц поведения, что, в свою очередь, ухудшает сопровождаемость теста. Например, если имеются два связанных сценария использования (допустим, регистрация и удаление пользователя).

  • подготовка — подготовка данных для регистрации пользователя;

  • действие — вызов UserController.RegisterUser();

  • проверка — запрос к базе данных для проверки успешного завершения регистрации;

  • действие — вызов UserController.DeleteUser();

  • проверка — запрос к базе данных для проверки успешного удаления.

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

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

Выводы

Интеграционные тесты проверяют, как ваша система работает в интеграции с внепроцессными зависимостями.

Интеграционные тесты покрывают контроллеры; юнит-тесты покрывают алгоритмы и доменную модель.

Интеграционные тесты обеспечивают лучшую защиту от багов и устойчивость к рефакторингу; юнит-тесты более просты в поддержке и дают более быструю обратную связь.

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

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

Неуправляемые зависимости — внепроцессные зависимости, доступные для других приложений. Взаимодействия с неуправляемыми зависимостями видны снаружи. Типичные примеры — сервер SMTP и шина сообщений.

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

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

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

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

Циклические зависимости увеличивают когнитивную нагрузку при попытках разобраться в коде. Типичный пример — обратный вызов (когда вызываемая сторона уведомляет вызывающую о результате своей работы).

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

Ссылки на все части

  • Анатомия юнит-теста

  • Аспекты хороших юнит-тестов

  • Для чего нужно интеграционное тестирование?

Источник: https://habr.com/ru/post/556002/


Интересные статьи

Интересные статьи

Максим Дубинин — основатель проекта GIS-Lab и директор компании NextGIS, которая разрабатывает одноименный продукт. Наверное, его имя известно в России почти любому, кто связан с темой ГИС. Кто...
Elasticsearch, вероятно, самая популярная поисковая система на данный момент с развитым сообществом, поддержкой и горой информации в сети. Однако эта информация поступает непоследовательно и дро...
USB Type-C Что такое USB Gen 1, Gen 2 и Gen 2×2? (предыстория от февраля 2019) Раньше найти USB побыстрее было просто: выбирайте USB 3.0 вместо USB 2.0. но теперь вам придётся разобраться в...
От скорости сайта зависит многое: количество отказов, брошенных корзин. Согласно исследованию Google, большинство посетителей не ждёт загрузки больше 3 секунд и уходит к конкурентам. Бывает, что сайт ...
В прошлом году правительство утвердило план мероприятий по направлению «Информационная безопасность». Это часть программы «Цифровая экономика Российской Федерации». В план внесли законопроек...