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

в 4:21, , рубрики: api, qa, qa automation, Блог компании 2ГИС, отладка, Тестирование IT-систем, Тестирование веб-сервисов

image

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

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

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

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

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

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

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

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

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

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

Все данные у нас хранятся в базе 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: Лишние тестовые данные

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

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

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

Ключей не так много — 1000 штук. Для ускорения работы приложения мы храним их в памяти и обновляем раз в несколько минут или по требованию. Таким образом, тесты после сохранения очередного ключа запускали процесс синхронизации, окончания которого мы не дожидались — получали в ответ «504», который писался в логи. При этом приложение никак не сигнализировало о проблеме и мы думал, что все у нас замечательно работает. Сам процесс регрессионного тестирования продолжался. И в итоге получалось, что нам всегда везло и наши ключи сохранялись.

Мы жили в неведении, пока не проверили логи. Оказалось, что ключи то мы создавали, но не удаляли после прогона тестов. Таким образом их у нас накопилось 500 000.

Не повторяйте наших ошибок: если вы как-то модифицируете БД в тестах, то обязательно позаботьтесь о том чтобы БД приводилось в первоначальное состояние. После того, как мы почистили базу, процесс обновления ключей ускорился в 500 раз.

Проблема 3: Cлучайная выборка данных

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

Мы очень любим проверять работу приложения на разных данных. Данных у нас очень-преочень много, и периодически находятся проблемы. Например, был случай, когда нам не выгрузили данные по рекламе, но тесты вовремя отловили эту проблему. Вот поэтому в каждом запросе наших тестов можно увидеть 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 раз.

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

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

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

Автор: Takumi

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js