Хабрахабр

Ограничения, которые нужно нарушать или как мы ускорили функциональные тесты в три раза

image

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

Коротко о приложении

Моя команда разрабатывает публичное API, которое предоставляет данные пользователям 2ГИС. Когда вы заходите на 2gis.ru и ищете «Супермаркеты», то получаете список организаций — это и есть данные с нашего API. На наших 2000+ RPS почти каждая проблема становится критичной, если ломается какая-то функциональность.

4. Приложение написано на Scala, тесты — на PHP, база данных — PostgreSQL-9. Нас продолжительность тестов особо не напрягала — мы привыкли, что на старом фреймоврке тесты могли идти 60 минут. Функциональных тестов у нас порядка 25000 штук, они проходят за 30 минут на специально выделенной виртуалке для общей регрессии.

Как мы ускорили и так «быстрые» тесты

Все началось случайно. Как обычно и бывает. Мы поддерживали одну фичу за другой, попутно пописывали тесты. Их количество росло и необходимое время на выполнение — тоже. Однажды тесты начали вылезать за отведенные им лимиты по времени, а следовательно процесс их выполнения завершался принудительно. Незавершенные до конца тесты чреваты пропущенной проблемой в коде.

Так началось исследование под названием «Тесты работают медленно — исправляй». Мы проанализировали скорость выполнения тестов и задача по их ускорению резко стала актуальной.

Ниже описаны три большие проблемы, которые мы нашли в тестах.

Проблема 1: Неправильно использовали jsQuery

В основном — в виде json, поэтому мы активно используем jsQuery. Все данные у нас хранятся в базе PostgreSQL.

Вот пример запроса, который мы делали в БД, чтобы получить нужные данные:

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() Например, был случай, когда нам не выгрузили данные по рекламе, но тесты вовремя отловили эту проблему.

Если говорить про пример выше, то без рандома он отрабатывает за 160ms. Когда посмотрели результаты запросов, с рандомом и без него с помощью EXPLAIN’a увидели прирост производительности в 20 раз. Мы всерьез задумались, что нам делать, потому что от рандома полностью отказываться не очень хотелось.

Мы решили сократить выборку до первых 100 фирм, подходящих под наши условия. Например, в Новосибирске порядка 150 тысяч фирм, и когда мы пытались найти фирму, у которой есть адрес, сайт и рубрика — то получали рандомную запись почти из всей базы. Итогом раздумий стал компромисс между постоянной выборкой разных данных и скоростью:

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 раз.

Еще раз краткий список действий:

  1. Если указываем несколько условий для выборки данных в поле JSONB, то их нужно перечислить в одном операторе ‘@@’.
  2. Если создаем тестовые данные, то обязательно их удаляем. Даже если будет казаться, что их наличие не влияет на функционал приложения.
  3. Если нужны рандомные данные для каждого прогона, то находим компромисс между уникальностью выборки и скоростью выполнения.

Теперь наши 25К тестов проходят за 10 минут. Мы в три раза ускорили прохождение регрессии благодаря простым (а для кого-то, наверное, даже очевидным) модификациям. Неизвестно, сколько неожиданных открытий нас еще ждет там. И это не предел — на очереди у нас оптимизация кода.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть