Как стать автором
Обновить

Комментарии 20

testcontainers как раз для изоляции тестов и придумали - все другие сценарии только если по каким-то причинам не подходит этот.
Параллельно тесты запускаются стандартными средствами JUnit 5 со свойством
junit.jupiter.execution.parallel.mode.classes.default = concurrent

а для доступа к неразделяемым ресурсам используется аннотация ResourceLock на тестах.

По поводу изоляции. Мне попадались примеры с testcontainers, когда делают копию базы на тест. Вы об этом? Я в статье написал вкратце в "Имеющиеся решения", 5* пункт.

> ResourceLock

Если всем 800 тестам нужна база (пример из проекта), то это не рановсильно последовательному запуску?

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

На каждый тест создается докер с базой

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

N — на моем рабочем маке 4, на CI — 8.

у вас задача быстро выкатить решение - вы его и пилите. Это не значит, что оно правильное. Ваш подход пока работает, но нарушает принцип изоляции тестов, что может привести к недетерминированному поведению тестов, за что вас проклянут потомки. Работает - ну и ладно, но если строить решение не оглядываясь на вечер пятницы, то это неверный подход. Но мы все понимаем, что "правильно" оторвано от реальности - если устраивает, то и хорошо.
Делать 800 копий базы - это канонично. База не должна быть большой для тестов, если это не специализированные тесты "проверим как там прод". Докеры легкие - контейнеры докера в принципе отличаются только данными, запихиваемыми в базу, так как уровни докера (не знаю, как по-русски) это разделяемая память.
И делать 800 в параллель не надо - жюнит настраивается, сколько тестов гнать в параллель.
Разделяемые ресурсы с локами - это не базы. Это если у вас недоинтеграционные тесты, например, часть из которых завязана на какой-нибудь внешний (необязательно) UserManagementService - вот эти тесты помечаем как нуждающиеся в локе и они не будут выполняться конкурентно (то есть будут только с теми, что не помечены)

но нарушает принцип изоляции тестов, что может привести к недетерминированному поведению тестов, за что вас проклянут потомки

А потом узнают, что можно добавить одну строчку в gradle:

forkEvery = 1

И заберут проклятья обратно.

Ну а если без шуток, я бы проверил вот это заявление про "Докеры легкие".
Если каждый тратит 1 секунду на тест, то для 800 тестов — это уже 13 минут. Сейчас с моим решением тесты около того и бегут.

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

а я бы проверил математику - 64 параллельных теста по 1 секунде, всего 800 тестов - это не 13 минут, а 12 секунд. Мы же про параллельность говорим? 64 много? Может, тесты тяжелые слишком? Может, поставим 32 параллельность? Уже 24 секунды. Но не 13 минут

Да, я написал "13 минут", но не дописал, что эту работу можно разделить по юнитам CPU. Все надо измерять. Если вы одновременно запустите 64 "секундных" теста, они не решатся за 1 секунду, если у вас не 64 юнита CPU (грубо говоря). Если у вас их 4, а задача — computationally intensive (а я полагаю, что создание тестового контейнера — такая), то тесты займут около 16 секунд.

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

Такое может произойти по двум причинам:

  1. Два теста меняют shared-ресурс (cache, db) одновременно;

  2. Один тест оставляет стейт, который мешает другому (например, из-за мемоизации).

Testcontainers помогают решить "1", но и подход, который я предложил, это так же решает. Т.к. в рамках одного процесса тесты выполняются последовательно (не будет одновременного доступа к базе).

Testcontainers не помогут решить "2", а `test.forkEvery = 1`, например, — поможет, т.к. каждый тест в своем процессе. Я не хочу сказать, что предлагаю так делать, просто рассуждаю.

С Testcontainers придется думать о том, как создаются сущности в приложении. Например, как создается connection pool. Если он уже singlton (так было в проекте, когда я брался за задачу), то придется переписывать. В подходе, который я предложил, нет параллельности в рамках процесса: т.е. не нужны ни логи, ни аннотации куда-то добавлять — вообще ничего. И тащить дополнительную зависимость в проект не нужно.

смотрим что у вас - форкаем базы на рандомный набор тестов. Отлови-ка, что там на что влияет.
смотрим на тестконтейнерс - управление разделяемыми ресурсами. Вы можете использовать совместно один докер контейнер с базой - это задокументировано. Уверены, что не нужет отдельный контейнер - успользуйте общий. Не уверены - сдалайте столько наборов тестов со своими докерами, сколько нужно.
Хотите один докер на тест - пожалуйста, хотите один на всех - берите, хотите пять на разные взаимоблокирующие наборы - вперед, хотите контурентные с одной базой - хватайте ResourceLock. Есть все

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

  1. В предложенном варианте я так понимаю базы копятся до конца и удаляются все в конце. При большом количестве тестов и ограниченности места это может быть проблемой. Я у себя эти базы удаляю в фоне сразу как они становятся не нужными (удаление старых баз не должно блокировать создание новых).

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

  3. Не все тесты мешают друг другу, и когда время выполнения становится критическим - тесты можно группировать, чтоб создать одну базу под несколько кейсов. Как правило создать одну базу с двумя кучками объектов под два теста быстрее чем создать последовательно создать две базы, по кучке на каждый тест (тут внимательный читатель скажет "а как же параллельность" и я должен признать что у меня с ней почти никак из-за того что тестируется GUI, основная схожесть моего подхода с описанным в статье в создании базы на тест)

  4. Просто полезная вещь - иметь выключатель не удаяющий базу для упавшего теста. Чтоб потом подключиться к этой базе и посмотреть почему упало.

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

  6. Тоже не связанное с постгресом - оказывается нельзя просто так взять и получить новую базу через единственный CREATE DATABASE стейтмент в Оракле. Конкретно под Оракл пришлось менять подход на создание временного пользователя/схемы. Вначале я думал что я чего-то просто не знаю, но потом наткнулся на исходники одного из dbfiddle и там товарищи делали так же.

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

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

4. Просто полезная вещь - иметь выключатель не удаяющий базу для упавшего теста. Чтоб потом подключиться к этой базе и посмотреть почему упало.

В примере, что я привел, можно закомментировать (или еще лучше — завязать на флаг) finilizedBy.

Еще в статье не написал, но в примере проекта так сделал — докер образу с постгресом передал флаг, чтобы печатал все логи по

  postgres:
    image: postgres:11
    command: -E # prints all the statement; slow but useful for debugging

Пробовали так делать в Wargaming. Тесты в PostgreSQL. В результате пришли к выводу, что усложнение слишком существенно, а прирост скорости в ~ 2-3 раза, а когда у тебя 9 минут все тесты, то игра не стоит свеч. Гораздо проще ускорить запуск тестов, что и было сделано:

  1. Запрофилировали все тесты, нашли самые долгие, ускорили.

  2. Ускорили всю работу с БД, для этого на локальных тачках отключили fsync, а потом и вовсе научились создавать БД под тесты, используя отдельный in-memory template, который клал БД в отдельное место, которое было в tmpfs. Вторую оптимизацию применили и на сервере, который тестировал PR.

    На выходе: Получили тоже ускорение в 2-3 раза, но всё поведение БД было как в реальном проде. И без необходимости подкладывать костыли при изменении тестов.

А почему усложение существенное? Там по тестам — 2 файла дополнительно пишутся и несколько строк в build.gradle.

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

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

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

А вот ещё. Как-то мы тогда сделали, что разработчики бы заметили, что тесты запустятся параллельно. Проект местами был тяжёлый, использовался PostGIS, и бывали случаи падения по ресурсам от излишней параллельности. 8 лет назад ресурсы ещё были не бесконечны. Сегодня даже на ноутах я думаю мы бы справились.

А разбивать её тесты нельзя было, нарушалась удобная навигация по проекту.

Gradle вроде рандомно выбирает тесты, самому ничего разбивать по папкам не надо.

бывали случаи падения по ресурсам

Тут тоже, на CI 16gb ram давалось на тесты. Но это решилось нахождением утечки памяти.

Gradle вроде рандомно выбирает тесты, самому ничего разбивать по папкам не надо.

У нас было сложнее. Тесты разбиты на TestSuite`ы и только он может поднять нужную игровую ситуацию. А соответственно есть очень жирные TestSuite, которые дробить нежелательно.

А у нас бд snowflake. Никаких контейнеров. Вот буду делать для нее распаралеливанте. Кто сталкивался с такой задачей, поделитесь опытом

Я привел код с контейнерами, но по сути, мало что изменилось бы, если бы база была локально установлена. Так же бы создавал копии базы и чистил. Думаю, snowflake в этом плане ничем не отличается: погуглил, попалось Zero Copy Clone Snowflake, можно в эту сторону покопать.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории