Pull to refresh

Ускорение тестирования Django-проектов

Reading time6 min
Views4.9K
Вопросу тестирования Django-приложений уделено много внимания в различных статьях, в том числе и на Хабре. Почти в каждой из них хотя бы пара предложений посвящена способам и хакам для ускорения прохождения тестов, и поэтому сказать что-то принципиально новое здесь непросто.

В проекте панели управления хостингом, разработкой которой я занимаюсь значительную часть времени своей работы в NetAngels, насчитывается 120 таблиц и при тестировании загружается порядка 500 объектов из fixtures. Нельзя сказать, что это пугающе много, однако создание всех таблиц, добавление индексов и загрузка объектов при каждом запуске теста довольно сильно напрягают, особенно, если запускается всего один или пара тестов.

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

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

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

Небезопасные настройки транзакций


Всем плевать, если в процессе тестирования внезапно пропадет электричество, и данные не будут до конца записаны на диск. Обычно же сервера БД настроены таким образом, чтобы минимизировать печальные последствия подобных событий. Если для тестов у вас используется отдельный MySQL или PostgreSQL сервер, можете смело менять настройки на небезопасные.

Для MySQL, в частности, предлагают следующее:

[mysqld]
default-table-type=innodb
transaction-isolation=READ-COMMITTED
innodb_flush_log_at_trx_commit = 0
skip-sync-frm=OFF


В PostgreSQL для тех же целей рекомендуют добавлять опцию fsync = off в postgresql.conf.

Использование ramdisk


Более радикальный подход — вообще не использовать диск, а вместо этого работать с данными в оперативной памяти. Общий принцип заключается в том, что создается новый раздел с файловой системой tmpfs, монтируется в отдельный каталог, а затем все файлы базы создаются в этом каталоге. Дополнительный бонус — простое размонтирование раздела удаляет все данные.

SQLite для тестов


На самом деле, разработчики Django уже сделали очень много для того, чтобы тесты выполнялись как можно быстрее. В частности, если вы используете в качестве движка базы SQLite, то в процессе тестирования база создается в памяти (в качестве имени файла драйверу передается строка ":memory:"), и одного этого способа уже достаточно, чтобы наверняка решить большинство проблем со скоростью.

Иногда жалуются на то, что ORM Django недостаточно тщательно скрывает детали работы базы данных, и поэтому в некоторых случаях может оказаться так, что код, который работал в SQLite (т.е. тесты проходили), внезапно ломается при выкатывании на систему, где живет и работает MySQL. Действительно, такое иногда случается, но как правило, является следствием того, что вы сделали что-то «необычное», например, принялись вручную составлять запросы с помощью метода QuerySet.extra. Однако, скорее всего, если вы делаете такие вещи, то вы знаете, чем это может грозить.

Предварительное создание тестовой базы SQLite


Как известно, при запуске тестов Django выполняет следующую последовательность действий:

1. с разрешения пользователя очищает тестовую базу данных, если в ней что-то есть
2. создает все таблицы и индексы в тестовой базе данных
3. загружает набор fixtures с именем initial_data
4. выполняет тесты, один за другим
5. удаляет всё, что было создано в процессе выполнения тестов

Первый и последний этап могут не выполняться, если данные живут в памяти, а вот шаги 2 и 3 занимают существенное количество времени и сбивают высокий темп итераций «поправил код — запустил тест». Очевидно, схема базы данных и набор fixtures меняется значительно реже, чем код и тесты, поэтому имеет смысл каким-то образом сэкономить на постоянном создании базы. Кстати, шаги 2 и 3 — это обычное выполнение management-команды «syncdb».

Общий подход для ускорения запуска тестов состоит в том, чтобы выполнять syncdb вручную, и только тогда, когда это действительно необходимо, а при запуске тестов просто копировать заранее подготовленную базу данных. Используя SQLite, можно было бы обойтись копированием файлов, но не хотелось и терять преимущества работы с тестами в ":memory:".

Недолгий поиск показал, что решение на этот случай существует. Оказывается, SQLite имеет интерфейс для «горячего» резервного копирования (совсем как у взрослых), и если мы перед запуском тестов выполним такое копирование из заранее подготовленной базы в базу с именем :memory:, мы получим как раз то, что нужно: инициализированную базу данных в памяти.

Первая сложность в реализации заключается в том, что стандартный модуль Python sqlite3 не поддерживает, и возможно, никогда не будет поддерживать этот API, поэтому для выполнения такого копирования средствами Python предлагают воспользоваться сторонним модулем с именем APSW (Another Python SQLite Wrapper).

Вторая сложность заключается в том, что каждое новое соединение с базой данных в :memory: создает свою собственную копию базы (очевидно, пустую), и поэтому надо каким-то образом научить курсор, который используется ORM, использовать соединение, инициализированное APSW. К счастью, на этот случай предусмотрен хак: вместо строки с именем файла при создании соединения средствами sqlite3 можно передать объект apsw.Connection, который будет проксировать все запросы самостоятельно.

Таким образом, решение выглядит очень просто:

1. Создаем два объекта ASPW Connection, один из которых ссылается на заранее подготовленную базу данных, а второй на базу в памяти.
2. Копируем данные из файла в память.
3. В качестве параметра NAME для алиаса с именем «default» передаем ASPW Connection, ссылающийся на память.
4. Инициализируем курсор, и запускаем тесты.

Базу данных готовить очень просто: достаточно в переменную DATABASES settings.py добавить еще один алиас с именем «quickstart», а затем выполнить ./manage.py syncdb --database quickstart.

Весь код, который может выполнять эти действия, занимает чуть больше 20 строчек и приведен ниже. Для того чтобы заставить его работать, достаточно

1. Установить APSW
2. Скопировать код в отдельный файл и поместить его в проект
3. Добавить в settings.DABABASES алиас для базы данных с именем quickstart
4. Создать базу данных, выполнив ./manage.py syncdb --database quickstart
5. Установить переменную TEST_RUNNER таким образом, чтобы она ссылалась на класс только что сохраненного объекта
6. Попытаться запустить какой-нибудь простой тест.

Copy Source | Copy HTML
  1. import apsw
  2. from django.test.simple import DjangoTestSuiteRunner
  3. from django.db import connections
  4.  
  5. class TestSuiteRunner(DjangoTestSuiteRunner):
  6.  
  7.     def setup_databases(self, **kwargs):
  8.         quickstart_connection = connections['quickstart']
  9.         quickstart_dbname = quickstart_connection.settings_dict['NAME']
  10.  
  11.         memory_connection = apsw.Connection(':memory:')
  12.         quickstart_connection = apsw.Connection(quickstart_dbname)
  13.         with memory_connection.backup('main', quickstart_connection, 'main') as backup:
  14.             while not backup.done:
  15.                 backup.step(100)
  16.  
  17.         connection = connections['default']
  18.         connection.settings_dict['NAME'] = memory_connection
  19.         cursor = connection.cursor()
  20.  
  21.     def teardown_databases(self, old_config, **kwargs):
  22.         pass


В результате время выполнения одного единственного теста у меня сократилось с 18 до 2 секунд: вполне комфортно для того, чтобы запускать тест как часто, как хочется.

Этот же самый код, но с комментариями и с жирным предупреждением «your tests can eat your data!» (используйте только в тестовом окружении) выложен на gist.github.com/1044215.

Надеюсь, эти несложные рекомендации позволят вам писать код быстрее, эффективнее и надежнее.

Использованные источники


За деталями и прочей полезной информацией рекомендую обращаться к документации по вашему серверу БД, а также к следующим источникам:

Speeding up Django unit test runs with MySQL
Innodb Performance Optimization Basics
Using the SQLite Online Backup API
How to use SQLite’s backup in Python
Tags:
Hubs:
Total votes 33: ↑30 and ↓3+27
Comments11

Articles