Интро
Вы все еще разрабатываете и тестируете в общем окружении, пересылаете в мессенджерах файлы конфигов для запуска приложения на рабочей машине, провели половину спринта в ожидании ресурсов для новой: бд, очереди, etc.? Знайте - вы не одиноки. Но бывает по-другому.
Если вы еще здесь, полагаю, что все же хочется “по-другому”. На самом деле это вовсе не значит, что сейчас плохо. Просто жизнь такая.
Так о чем это мы тут? О рабочем и тестовом окружении, интеграционном (здесь будем называть интеграцией любое внешнее по отношению к процессу приложения взаимодействие – потому что так хочется) тестировании и немного о процессе разработки по.
Разработка как она есть
В начале были тесты (а может и не тесты, а может и не будут) и тесты требовали интеграций. И это проблема. Тем, кто на своей рабочей машинке запускал несколько инстансов db2, конфигурировал контейнер сервлетов, поднимал пару-тройку web сервисов, а еще поднимал сassandra, и тогда… в общем кто помнит тому уже не забыть.
Представим, что вы как гипотетический разработчик хотите дотащить фичу до релиза. Для этого вам нужно развернуть зоопарк зависимостей в разных конфигурациях в локальном окружении, написать тесты, имплементировать функционал, пройти несколько уровней тестирования, по ходу дела поправить баги, наконец задеплоиться на прод и, барабанная дробь, скрестив пальцы надеяться, что ничего не навернулось по пути. Это же кошмар.
А что нельзя на ci? А ci занят, нет данных, не те конфиги, не те зависимости, etc.. Часто (очень часто) ci становился узким местом разработки. Кстати, иногда так происходит по объективным причинам. С трудом могу себе представить неограниченное количество СХД выделенных под разработку и тестирование. Дорого. Очень дорого.
(Не) Бест практисес
Конечно, были и есть бест практисес. Вот некоторые известные мне способы порешать вышеописанные проблемы.
Используйте реальные экземпляры управляемых зависимостей; неуправляемые экземпляры заменяйте моками. Годный совет. Не так ли?
Сохранение схемы взаимодействий с неуправляемыми зависимостями обусловлено необходимостью поддержания обратной совместимости с такими зависимостями. Моки идеально подходят для этой задачи. Они позволяют обеспечить неизменность схемы взаимодействий в свете любых возможных рефакторингов.
Но моки не панацея, а иногда просто опасны. И вот почему:
Часто вы не обладаете глубоким пониманием того, как работает сторонний код;
Даже если код уже предоставляет встроенные интерфейсы, мокирование этих интерфейсов часто сопряжено с риском, потому что вы должны быть уверены в том, что поведение мока совпадает с тем, что фактически делает внешняя библиотека;
Моки могут скрывать дефекты.
Порой мокировать сложно или даже невозможно.
А как вам совет использовать ин-мемори базы данных?
Но! Функциональность бд в памяти сильно отличается от традиционных баз данных. А значит, здесь возникает проблема несоответствия между рабочей и тестовой средой. Такте тесты никогда не обеспечат хорошей защиты, и в конечном итоге вам придется проводить регрессионное тестирование вручную.
Поэтому большинство все же склонялось к использованию реальной бд и на это как мы видим были веские причины.
Однако совместная база данных создает проблему изоляции интеграционных тестов друг от друга. И таким образом никак не ускорят тестирования, а общий стейт делает разбор сработавших тестов тем еще квестом.
А что делать с очередями?
С очередями вообще все сложно и просто одновременно – они есть, ими нужно пользоваться.
Выходит что с следуя годным советам мы с одной стороны упростили рабочее окружение и в тоже время получили ненадежные тесты и тем самым были вынуждены перевенуть пирамиду тестирования (по меньшей мере превратить ее в прямоугольник) т.к. то что не было проверено на нижних уровнях либо проверяется на верхних либо никак - т.е. в проде (идея в том что тесты можно двигать по уровням и в целом скипать уровни кроме уровня интеграции/системного тестирования в пре-прод окружении).
Таким образом интеграционное и системное тестирование стало превращаться в монстра так как приняло на себя всю сложность окружения, отсутствие контроля над ним (включая отсутствие контроля над данными); число кейсов стало больше, а проблемы общих контуров и другие никуда не делись.
Было очевидно, что ресурсы простаивают, развертывание занимает неприлично много времени, а проблемы окружения не должны влиять на бизнес задачи.
Мрачновато. А как же быть?
Появился docker. До него кончено были, виртуалки (скорее всего еще что-то менее популярное), но это не наш вариант.
А значит настало время упростить развертывание, сделать окружение заслуживающим доверия, обеспечить независимость, и управляемость, согласованность состояния тестируемого приложения, перестать абьюзить общие стенды, начать пользоваться ci и таки постараться получить из тестов пирамиду.
Docker и Docker compose
Так вот, появился Docker. Но что интереснее буквально в следующем за годом выхода Docker зарелизился и Docker compose.
Докер позволил отделить приложение от инфраструктуры. Управление инфраструктурой, по сути, стало напоминать управление приложением. Docker предоставил возможность упаковывать и запускать приложения в контейнерах - независимом, изолированном окружении.
А compose унифицировал определение и управление многоконтейнерными приложениями с помощью единственного YAML файла. Взяв на себя довольно сложную, задачу оркестрации и координации различных сервисов и упростил управление и тиражирование инфраструктуры приложения.
Docker проник везде от локальных машин до ci job runner’ов. Фреймворки автоматизации UI тестов, всегда сильно зависевшие от окружения (установленных браузеров etc.) одни из первых, осознали возможности и появился например, github.com/aerokube/selenoid. Docker-in-docker воцарился на ci.
Compose и сейчас является основным средством для разработчиков, тестировщиков и devOps инженеров. Это универсальное средство коллаборации в проекте в идеале призванное упростить работу с приложением на всех этапах от разработки до вывода в продакшн.
Однако нельзя не заметить, что Docker, compose и его скрипты всегда оставались и остаются чем-то внешним по отношению к приложению и его жизненному циклу. Особенно это становится очевидно в интеграционном тестировании и разработке в локальном окружении.
Как и в какой момент разворачивать и останавливать работу окружения, поднятого для интеграционных тестов? Как вписать Docker, compose в maven build lifecycle, в конце концов какие и сколько docker-compose.yml файлов мне нужно?
Docker и compose это огромное достижение. От разработки до ci и прода Docker используется везде (и даже на windows машинах). Окружение развернутое в Docker управляемое и заслуживает доверия. То, что Docker воспринимается как нечто внешнее (чем он и является) по отношению к приложению не может перевесить очевидных плюсов от его использования. Возможности предоставляемые Docker важно и нужно использовать в процессе разработки.
Здесь позволю себе напомнить о выдавливании тестовых сценариев на системный уровень, и о ботлнеках на ci. Использование short-lived, изолированных окружений и контроль над окружением вкупе с управлением данными позволяют преодолеть обе проблемы, описанные выше. Кто бы мог подумать?...
Тестконтейнеры
Таки шо за тест контейнеры?
Как мы подметили при использовании Docker возникают вопросы:
Как и в какой момент разворачивать и останавливать работу окружения, поднятого для интеграционных тестов?
Как вписать Docker, compose в maven build lifecycle, в конце концов какие и сколько docker-compose.yml файлов мне нужно?
Как мне подружить мой фреймворк разработки и тестирования с Docker?
Почему я должен переключать контекст с написания и тестирования фичи на конфигурацию энва, и программировать в yaml?
etc.
Testcontainers позволяют закрыть вопросы выше и интегрировать Docker и compose в код вашего приложения, при этом получить поддержку фреймворка тестирования, а иногда и разработки, как например в Spring boot 3. Testcontainers не что-то абсолютно новое и загадочное, это мостик между кодом вашего приложения и Docker.
Testcontainers это библиотека тестирования с простым и легковесным API предназначенная для предварительной загрузки компонентов интеграционных тестов - реальных сервисов завернутых в Docker контейнеры.
Используя Testcontainers можно писать тесты, позволяющие тестируемому приложению взаимодействовать с зависимостями того же типа что и в проде, без необходимости прибегать к помощи моков или in-memory базам данных.
Ни тебе лишних и порой опасных моков ни переусложненных скриптов развертывания. Все необходимые зависимости можно описать прямо в коде тестов, а затем просто запустить их. Все необходимые контейнеры будут сперва созданы, а затем удалены автоматически.
Все что вам понадобится это Docker.
Что вы получаете если используете Testcontainers:
Нет необходимости в заранее подготовленном тестовом окружении. Testcontainers поднимают все необходимые зависимости перед запуском тестов. А код описания инфраструктуры расположен бок о бок с кодом тестов.
Не нужно беспокоиться о том, что несколько параллельных пайплайнов могут повлиять друг на друга из-за разделения ими общего состояния – сервиса, бд, т.к. каждый пайплайн использует свой собственный уникальный, изолированный набор зависимостей.
Тесты можно запускать прямо из IDE, так же как запускаются обычные unit тесты. Не нужно пушить изменения и ждать пока тесты пробегут на СI.
После тестового прогона, Testcontainers позаботится об очистке окружения самостоятельно.
Библиотека поддерживается очень весомыми спонсорами (уважаемыми людьми) и имеет кучу реализаций и интеграций со всевозможными языками и фреймворками как тестирования, так и разработки. Однозначно стоит посетить их страницу на github https://github.com/testcontainers и пробежаться по документации https://testcontainers.com, чтобы, так сказать, почувствовать вкус (вкус контейнера буээээ или вкус свободы).
Немного об интеграциях
И все же запуск тестов и приложения — это разные вещи. В Spring, например тестовый контекст может предельно сильно отличаться от боевого. Classpath тестового приложения другой.
Ребята из Spring подумали об этом за нас и сделали возможным стартовать приложение, контекст которого смержен из тестовых и боевых бинов с уважением к testcontainers. Эдакий франкеншейн, но для тестирования норм. Да-да, вы просто поднимаете приложение, а например контейнер с postresql для вас запустит связка Spring+testcontainers и даже проперти засетит нужные.
Таким образом testcontainers не только про тестирование, но и про локальное рабочее окружение.
Примеры такого подхода можно найти во многих open source репозиториях. А если пройти по ссылке можно почитать, за счет чего такая интеграция стала возможной.
Так, а что с моками?
Дело в том, что моки могут как опережать, так и отставать по функционалу от целевого приложения. Все зависит от выбранной стратегии интеграции и циклов разработки компонентов. И это нормально.
Некоторые edge cases без моков просто не проверить.
Полностью отказываться от использования моков не имеет смысла. А что имеет так это рассматривать моки как любые другие зависимости приложения и использовать для них Testcontainers.
Закругляю
Несомненно, Testcontainers заслуживают того, чтобы дать им шанс в проекте. Как по мне вопрос выбора между Docker compose и Testcontainers не должен быть ультимативным. Разные роли в проекте предполагают разные сценарии использования, а интеграции проектов вообще целая вселенная. Тут нет серебряной пули.
Так что в реальной жизни вероятнее всего получить комбо из Testcontainers и Docker compose. Как не удивительно, но и так можно. За подробностями обращайтесь к документации модуля Docker compose.
Подводя итог, отметим, что Testcontainers бережет нервные клетки, зеленит ci джобы и растит маржинальность (но это не точно). Шучу. Просто думается, что профит уже и так очевиден. Как писали классики: "пилите, Шура, пилите".
Перспективы
Даже несмотря на возможные ограничения legacy зависимостей и архитектуры Docker и Testcontainers прокачивают наш процесс и похоже являются одним из признаком зрелости команды.
Как нетрудно догадаться тестируемое приложение и его окружение в Testcontainers и docker на данном этапе все еще сильно отличаются от того, что предложит реальная жизнь, но уже достаточно надежны и достоверны, а поэтому ценны.
Из этого также следует невозможность исключения стадии развертывания и тестирования приложения с реальными зависимостями в окружении максимально приближенном к продакшену.
Но это уже совсем другая история.
evgeniy_kudinov
Опыт применения Testcontainers оставил хорошие впечатления для интеграционного тестирования и как для быстрого разворачивания локально инфраструктуры проекта через код.