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

Одной из наиболее распространенных причин медленных наборов тестов, особенно в больших приложениях, является количество взаимодействий с базой данных, в частности количество созданных записей. Мы используем RSpec и FactoryBot, вероятно, наиболее распространенную настройку в приложении Ruby on Rails. FactoryBot — потрясающая библиотека, и я не могу вспомнить ни одного приложения, над которым я работал в прошлом, которое не использовало бы ее. Он предоставляет DSL для определения и использования тестовых фабрик, а также упрощает создание тестовых объектов для ваших спецификаций. К сожалению, это также позволяет очень легко определять фабрики, которые со временем будут генерировать много ненужных вызовов базы данных и замедлять работу ваших спецификаций, и именно это произошло в нашем проекте.

В следующих параграфах я покажу вам:

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

В чем проблема «Фабрики Каскад»

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

evilmartians.com — TestProf: хороший доктор для медленных тестов Ruby

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

session
├─ participants
│  ├─ account
├─ event
│  ├─ organization
│  │  ├─ owner
│  │  │  ├─ account
│  │  ├─ members
│  │  │  ├─ account

Это можно перевести в следующие определения модели и фабрики:

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

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

  • 1 организация
  • 1 владелец
  • 3 члена
  • 4 сеанса
  • 12 участников
  • 16 аккаунтов

Всего 38 записей, и все это скрыто за create(:session).

Это проблема «Заводского каскада».

Как проверить, есть ли у вас проблема «Заводского каскада»

В предыдущем абзаце мы видели, как мы можем проверить количество записей, созданных фабрикой, с помощью консоли rails. Это не очень удобно, не так ли. К счастью, нам не нужно делать это вручную, мы можем использовать Evilmartian’s Test Profiler, который распечатывает в конце каждого запуска теста всю необходимую нам информацию. После добавления test-prof в ваши гемы вы можете просто использовать FPROF=1 перед своей командой RSpec, и вы увидите заводское использование вашего тестового прогона.

Эта таблица показывает вам все фабрики, которые создали объекты, сколько экземпляров они создали и сколько из них были вызваны напрямую (верхний уровень). В этом случае непосредственно один раз вызывалась только фабрика сеансов, все остальные экземпляры были созданы как побочный эффект. В этом случае у нас есть 37/38 (~ 97%) экземпляров, созданных на каскаде.

Теперь, когда у нас есть инструмент для печати использования фабрики и количества экземпляров, созданных в каскаде, как мы можем использовать его для измерения работоспособности фабрики? Просто следуйте приведенному выше примеру. Создайте новый файл спецификаций и добавьте один экземпляр фабрики. Затем запустите спецификацию, используя тег FPROF=1, и проверьте вывод.

Из вывода factory profiler вы можете сразу распознать каскад factory, проверив, сколько всего записей было создано, вызвав только factory без каких-либо опций.

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

Как решить проблему «Заводского каскада» итеративно

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

В Livestorm мы придумали следующий план:

  1. Дублировать фабрику сеансов в устаревшую фабрику сеансов.
  2. Переместите все ассоциации из атрибутов фабрики сеансов по умолчанию / обратных вызовов after_create в черты
  3. Для всех неверных спецификаций замените фабрику сеансов на устаревшую фабрику сеансов.
  4. Вручную заменить использование устаревшей фабрики сеансов на фабрику сеансов и исправить спецификации

Давайте посмотрим на это подробнее.

Дублировать фабрику

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

Переместите ассоциации на черты

Фабричные каскады генерируются ассоциациями, созданными по умолчанию на наших фабриках. Некоторые из них могут быть необходимы, например. обязательная ассоциация own_to, другие необязательны. В нашем примере множество ассоциаций, созданных в блоке after(:create), — это все объекты, которые могут быть полезны для некоторых спецификаций, но не нужны для корректной сессии. Это тип ассоциаций, которые вы не хотите создавать по умолчанию в своей фабрике, а только сохраняете их как необязательную черту, которую вы можете вызывать при необходимости. Давайте посмотрим, как преобразовать фабрику сеансов.

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

У нас еще есть много места для улучшения других фабрик, но уже с этим небольшим изменением мы удалили 24 ненужных вставки базы данных из фабрики по умолчанию, уменьшив процент каскадирования с 97% до 92% в этом отдельном файле. Если эта фабрика часто используется в вашем наборе тестов, создание более чем на 20 записей меньше при каждом создании фабрики может сильно повлиять на общее время выполнения.

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

Исправление ошибочных спецификаций с устаревшей фабрикой

После изменения поведения спецификации по умолчанию многие спецификации начнут давать сбои, потому что они ожидают существования связанных записей. Например. у вас может быть тест, который вместо явного создания участника сеанса в настройках теста берет его из списка существующих участников сеанса (session.participants.take). Поскольку оптимизированная спецификация больше не создает участников по умолчанию, эти спецификации начнут давать сбои. В большой кодовой базе количество таких провальных спецификаций может быть огромным, в нашем случае у нас было несколько сотен провальных спецификаций, и все их нужно исправлять вручную. Исправление их всех одновременно потребует много времени, а приложение продолжает развиваться вместе со спецификациями. Это привело бы к большому количеству конфликтов слияния в конце, что очень затруднило бы завершение оптимизации и выпуск кода. По этой причине мы создали копию старой фабрики в начале, чтобы мы могли просто заменить имя фабрики на устаревшее и снова передать спецификации. Конечно, вы не хотите навсегда хранить эту ссылку на устаревшую фабрику в своей кодовой базе, но теперь, когда у вас есть оптимизированная фабрика и переданные спецификации, вы можете объединить свою оптимизацию и заняться удалением устаревших ссылок на фабрики в маленькие шаги.

Избегайте создания фабричных каскадов: сделайте проблему очевидной

Одна из причин, по которой фабричный каскад встречается так часто, заключается в том, что вы на самом деле не замечаете его, пока ваш набор тестов не станет настолько медленным, что вам нужно изучить его, и только тогда вы обнаружите, что создаете, возможно, сотни записей. когда вам нужно только 1. Чтобы не доходить до этого момента и как можно скорее заметить, что фабрика создает слишком много ненужных записей, вы можете настроить свою среду разработки на автоматическую печать использования фабрики в конце каждого теста. Например. в VSCode вы можете использовать расширение Vscode-run-rspec-file **** для запуска спецификаций из вашей IDE с помощью сочетания клавиш и настроить команду, используемую для запуска спецификаций, для включения тега FPROF.

Краткое содержание

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

Наш набор модульных тестов перед оптимизацией создавал 150 000 записей в базе данных, 93% из них были созданы каскадно, и потребовалось около 9 часов вычислений (разделено на 150 заданий, выполняющихся параллельно). После оптимизации процент каскадирования снизился до 67%, количество создаваемых записей сократилось до 100 000, а время выполнения — до 7 часов. Стоит отметить, что мы не удаляли никаких спецификаций и не меняли саму спецификацию. Единственное изменение произошло в определении фабрики и в том, как фабрики используются для настройки тестовых данных.

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

Рекомендации

Этот проект по оптимизации тестов был сильно вдохновлен статьей в блоге Evil Maritans TestProf II: фабричная терапия для ваших тестов Ruby.