В рамках проекта цифровой модернизации для одного из наших клиентов возникла задача миграции данных из одной модели хранения в другую. Для тестирования такого решения мы обратились к практикам BDD (Behaviour Driven Development) и виртуализации зависимостей с помощью docker контейнеров. В данном посте рассмотрен рецепт как можно организовать тестирование подобного решения с помощью pytest-bdd и testcontainers на python. Весь исходный код доступен по ссылке. Как видно из диаграммы выше, источник данных живет в Azure. Миграция подразумевает перенос (копирование) актуальных данных из базы в Google Cloud сервис Datastore. API развертывается через Cloud Build как Cloud Run сервис. В ходе анализа схемы мы выделили ряд доменов, которые подлежат миграции в новое хранилище. Каждый домен представлен набором таблиц и не всегда данные должны оказаться в целевом хранилище целиком: порой это целые таблицы, а иногда определённая выборка атрибутов из неё. В нашем примере будет использоваться домен Posts. Информация о постах разбросана по следующим таблицам: post (id, title, summary) - общие сведения о постах category (id, title) - общие сведения о категориях post_category (id, postId, categoryId) - связующая таблица для постов и категорий post_meta (id, postId, key, content) - метаданные относящиеся к посту В Google Datastore всё это будем хранить в сущностях вида Posts. Следует отметить, что категории и метаданные относящиеся к посту в MSSQL хранятся в различных таблицах, но в Datastore они будут храниться в определенных полях - meta и categories Можно оценить, что разработка такого решения сводится к написанию SQL запросов различного уровня сложности, каких-то дополнительных преобразований (случай метаданных) и корректному отображению данных. В качестве одного из вариантов можно было бы рассмотреть покрытие миграции smoke тестами. Для этого пришлось бы на каждое изменение SQL запроса или другой логики развёртывать новую версию API, прогонять эти тесты, и, в случае падения, искать причины в логах. От инфраструктуры клиента в Azure (VPN + MSSQL) можно было бы отвязаться использованием Google Cloud SQL. Но, в любом случае, этот feedback loop не то, чего нам хотелось бы в силу его полной зависимости от Cloud-провайдера, что несёт за собой: временные затраты на развёртывание дополнительные ресурсы на поддержку инфраструктурного кода ручное сканирование лога на предмет причины падения тестов Проблему зависимости от Cloud можно решить, развернув setup локально. По сути, у нас здесь два ключевых компонента - это MSSQL и Google Datastore. Давайте рассмотрим как запустить эти вещи локально. На помощь приходит библиотека testcontainers. Это небольшая библиотека, позволяющая запускать docker контейнеры для тестирования. В состав уже входит набор некоторых готовых классов-обёрток для популярных решений. А так же имеется удобный API для собственных нужд. Взглянем на следующий код: Здесь, мы подготавливаем контейнер с MSSQL. Обратим внимание, что для fixture используется scope=session. По умолчанию, pytest использует scope=function, что предполагает вызов функции на каждый тест. Это можно было бы использовать, но запускать контейнер под каждый тест весьма расточительно. Хотя, в этом случае, можно не беспокоиться об изоляции тестовых данных друг от друга, т.к. каждый раз у нас будет свежий контейнер без данных. Но так как мы заинтересованы в том, чтобы сократить время выполнения всех тестов (а чем их больше, тем это чувствительнее), то будем использовать session. В результате контейнер будет создан лишь один раз перед запуском всех тестов. Но в таком случае возникает проблема изоляции данных. Оставлять данные после отработки теста нам не интересно, поэтому решать эту проблему будем удалением - запомним таблицы, которые заполнялись перед запуском теста и очистим после. Для Datastore контейнера напишем свой класс. Отмечу, что Google предоставляет набор эмуляторов для некоторых своих сервисов. В список поддерживаемых сервисов входит и Datastore. Наш класс базируется на image от Google со всеми необходимыми зависимостями.  Подготовим контейнер: Таким образом, перед запуском тестов у нас есть два готовых к работе контейнера. Можно приступать к тестам. Рассмотрим подход к написанию тестов. BDD - это эволюция практики TDD, где вводится новый концепт behaviour. Мы не будем в деталях останавливаться здесь на особенностях этой практики, отметим лишь два важных момента: мы по-прежнему остаёмся в рамках парадигмы Test First тесты становятся своего рода документацией, к которой могут обращаться и не технические специалисты (в нашем проекте это было важно) Два зайца одним выстрелом: тестируем код и пишем документацию. Подход к реализации behaviours будет зависеть от выбора конкретной библиотеки. В целом, эти подходы похожи. Наш выбор упал на плагин для pytest который реализует подмножество языка Gherkin - pytest-bdd. Этот плагин позволяет описывать тесты на human-friendly языке с помощью зарезервированных синтаксических конструкций Given/When/Then и использовать pytest runner для запуска. Тестирование нашей миграции подразумевает следующее: наличие определенного набора данных в MSSQL (Given data in MSSQL) непосредственный запуск миграции (When migration triggered) валидация новых данных в Datastore (Then check data exists in Datastore) Рассмотрим наш feature-файл с тестами: Здесь у нас два сценария: В первом сценарии мы мигрируем пост с которым связаны две категории. Эти данные разбросаны по трём разным таблицам. В конструкции Given мы указываем имя таблицы и используем JSON формат для описания данных. Этого достаточно для того, чтобы вставить запись в нужную таблицу:  Заполняем таблицы и запускаем миграцию. После чего, ожидаем создание сущности в Datastore - для этого необходимо указать содержимое сущности, для описания которого снова используется JSON:  Как было отмечено выше, мы хотим зачищать данные после выполнения каждого теста. Для этого можно воспользоваться механизмом hooks которые добавлены в pytest-bdd - pytest_bdd_after_scenario, или добавить финальную фразу непосредственно в сценарий. Дело вкуса. Мы выбрали второй вариант, где перед зачисткой дополнительно проверяем количество перенесённых сущностей. В примере используется одна, но в реальности может быть и набор из нескольких, поэтому финальная конструкция параметризована количеством сущностей: Во втором сценарии мигрируем пост с метаданными. В MSSQL это фиксируется в другой таблице - post_meta. В перенесённой сущности - в свойстве meta. Этот подход позволяет нам эффективно тестировать миграции данных, при этом, сопровождая сервисы необходимой документацией. В конечном итоге мы избавились от зависимости к Cloud-провайдеру, что существенно снизит временные затраты на запуск тестов и сэкономит ресурсы. Надеюсь, статья окажется полезной при решении похожих задач.  Google Cloud Engineer