Функциональные тесты — вещь полезная. Поначалу много времени они не занимают, но проект растёт, и тестов нужно всё больше и больше. Терпеть замедление скорости доставки мы не были намерены и, собравшись с силами, ускорили функциональные тесты в три раза. В статье вы найдёте универсальные советы, однако, особый эффект вы заметите именно на больших проектах.
Коротко о приложении
Моя команда разрабатывает публичное API, которое предоставляет данные пользователям 2ГИС. Когда вы заходите на 2gis.ru и ищете «Супермаркеты», то получаете список организаций — это и есть данные с нашего API. На наших 2000+ RPS почти каждая проблема становится критичной, если ломается какая-то функциональность.
Приложение написано на Scala, тесты — на PHP, база данных — PostgreSQL-9.4. Функциональных тестов у нас порядка 25000 штук, они проходят за 30 минут на специально выделенной виртуалке для общей регрессии. Нас продолжительность тестов особо не напрягала — мы привыкли, что на старом фреймоврке тесты могли идти 60 минут.
Как мы ускорили и так «быстрые» тесты
Все началось случайно. Как обычно и бывает. Мы поддерживали одну фичу за другой, попутно пописывали тесты. Их количество росло и необходимое время на выполнение — тоже. Однажды тесты начали вылезать за отведенные им лимиты по времени, а следовательно процесс их выполнения завершался принудительно. Незавершенные до конца тесты чреваты пропущенной проблемой в коде.
Мы проанализировали скорость выполнения тестов и задача по их ускорению резко стала актуальной. Так началось исследование под названием «Тесты работают медленно — исправляй».
Ниже описаны три большие проблемы, которые мы нашли в тестах.
Проблема 1: Неправильно использовали jsQuery
Все данные у нас хранятся в базе PostgreSQL. В основном — в виде json, поэтому мы активно используем jsQuery.
Вот пример запроса, который мы делали в БД, чтобы получить нужные данные:
SELECT * FROM firm WHERE json_data @@ 'rubrics.@# > 0' AND json_data @@ 'address_name = *' AND json_data @@ 'contact_groups.#.contacts.#.type = “website”' ORDER BY RANDOM() LIMIT 1
Легко заметить, что в примере несколько раз подряд используется json_data, хотя правильно было бы написать так:
SELECT * FROM firm WHERE json_data @@ 'rubrics.@# > 0 AND address_name = * AND contact_groups.#.contacts.#.type = “website”' ORDER BY RANDOM() LIMIT 1
Такие недочеты не слишком бросались в глаза, так как в тестах мы не пишем руками все запросы, а вместо этого мы используем QueryBuilder’ы, которые сами компонуют их после указания нужных функций. Мы не задумывались о том, что это может влиять на скорость выполнения запросов. Соответственно, в коде это выглядит как-то так:
$qb = $this>createQueryBulder()
->selectAllBranchFields()
->fromBranchPartition()
->hasRubric()
->hasAddressName()
->hasWebsite()
->orderByRandom()
->setMaxResults(1);
Не повторяйте наших ошибок: при наличии нескольких условий в одно поле JSONB, описывайте их все в рамках одного оператора ‘@@’. После того, как мы переделали, мы ускорили время выполнения каждого запроса в два раза. Раньше на описанный запрос уходило 7500ms, а теперь уходит 3500ms.
Проблема 2: Лишние тестовые данные
Доступ к нашему API предоставляется по ключу, у каждого пользователя API он свой. Раньше в тестах часто необходимо было модифицировать настройки ключей. Из-за этого тесты падали.
Мы решили создавать несколько ключей с нужными настройками при каждом прогоне регрессии, чтобы исключить проблемы пересечения. А так как создание нового ключа не влияет на функциональность всего приложения, данный подход в тестах ни на что не повлияет. Жили в таких условиях около года, пока не начали разбираться с производительностью.
Ключей не так много — 1000 штук. Для ускорения работы приложения мы храним их в памяти и обновляем раз в несколько минут или по требованию. Таким образом, тесты после сохранения очередного ключа запускали процесс синхронизации, окончания которого мы не дожидались — получали в ответ «504», который писался в логи. При этом приложение никак не сигнализировало о проблеме и мы думал, что все у нас замечательно работает. Сам процесс регрессионного тестирования продолжался. И в итоге получалось, что нам всегда везло и наши ключи сохранялись.
Мы жили в неведении, пока не проверили логи. Оказалось, что ключи то мы создавали, но не удаляли после прогона тестов. Таким образом их у нас накопилось 500 000.
Не повторяйте наших ошибок: если вы как-то модифицируете БД в тестах, то обязательно позаботьтесь о том чтобы БД приводилось в первоначальное состояние. После того, как мы почистили базу, процесс обновления ключей ускорился в 500 раз.
Проблема 3: Cлучайная выборка данных
Мы очень любим проверять работу приложения на разных данных. Данных у нас очень-преочень много, и периодически находятся проблемы. Например, был случай, когда нам не выгрузили данные по рекламе, но тесты вовремя отловили эту проблему. Вот поэтому в каждом запросе наших тестов можно увидеть ORDER BY RANDOM()
Когда посмотрели результаты запросов, с рандомом и без него с помощью EXPLAIN’a увидели прирост производительности в 20 раз. Если говорить про пример выше, то без рандома он отрабатывает за 160ms. Мы всерьез задумались, что нам делать, потому что от рандома полностью отказываться не очень хотелось.
Например, в Новосибирске порядка 150 тысяч фирм, и когда мы пытались найти фирму, у которой есть адрес, сайт и рубрика — то получали рандомную запись почти из всей базы. Мы решили сократить выборку до первых 100 фирм, подходящих под наши условия. Итогом раздумий стал компромисс между постоянной выборкой разных данных и скоростью:
SELECT * FROM (SELECT * FROM firm_1 WHERE json_data @@ 'rubrics.@# > 0 AND address_name = * AND contact_groups.#.contacts.#.type = "website"' LIMIT 100) random_hack ORDER BY RANDOM() LIMIT 1;
Таким простым способом мы почти ничего не потеряли при 20-кратном ускорении. Время выполнение такого запроса равна 180ms.
Не повторяйте наших ошибок: этот момент, конечно, сложно назвать ошибкой. Если у вас действительно много тестов, всегда задумывайтесь, насколько вам нужен рандом в данных. Компромисс между скоростью выполнения запросов в базу и уникальностью выборки помогло нам ускорить SQL-запросы в 20 раз.
Еще раз краткий список действий:
- Если указываем несколько условий для выборки данных в поле JSONB, то их нужно перечислить в одном операторе ‘@@’.
- Если создаем тестовые данные, то обязательно их удаляем. Даже если будет казаться, что их наличие не влияет на функционал приложения.
- Если нужны рандомные данные для каждого прогона, то находим компромисс между уникальностью выборки и скоростью выполнения.
Мы в три раза ускорили прохождение регрессии благодаря простым (а для кого-то, наверное, даже очевидным) модификациям. Теперь наши 25К тестов проходят за 10 минут. И это не предел — на очереди у нас оптимизация кода. Неизвестно, сколько неожиданных открытий нас еще ждет там.
Автор: Takumi