Как запустить тестовую базу данных Django только в памяти?

125

Мои модульные тесты Django выполняются долго, поэтому я ищу способы ускорить это. Я подумываю об установке SSD , но знаю, что у него есть и недостатки. Конечно, со своим кодом я могу кое-что сделать, но я ищу структурное исправление. Даже запуск одного теста выполняется медленно, поскольку базу данных необходимо каждый раз перестраивать / переносить на юг. Итак, вот моя идея ...

Поскольку я знаю, что тестовая база данных всегда будет довольно маленькой, почему я не могу просто настроить систему, чтобы всегда хранить всю тестовую базу данных в ОЗУ? Ни в коем случае не трогайте диск. Как мне настроить это в Django? Я бы предпочел продолжать использовать MySQL, поскольку это то, что я использую в производстве, но если SQLite  3 или что-то еще упрощает это, я бы пошел по этому пути.

Есть ли у SQLite или MySQL возможность работать полностью в памяти? Должна быть возможность настроить RAM-диск, а затем настроить тестовую базу данных для хранения своих данных там, но я не уверен, как указать Django / MySQL использовать другой каталог данных для определенной базы данных, тем более, что он продолжает стираться и воссоздавал каждый прогон. (Я использую Mac FWIW.)

Leopd
источник

Ответы:

164

Если вы установите ядро ​​базы данных на sqlite3 при запуске тестов, Django будет использовать базу данных в памяти .

Я использую такой код в моем, settings.pyчтобы настроить двигатель на sqlite при запуске моих тестов:

if 'test' in sys.argv:
    DATABASE_ENGINE = 'sqlite3'

Или в Django 1.2:

if 'test' in sys.argv:
    DATABASES['default'] = {'ENGINE': 'sqlite3'}

И, наконец, в Django 1.3 и 1.4:

if 'test' in sys.argv:
    DATABASES['default'] = {'ENGINE': 'django.db.backends.sqlite3'}

(Полный путь к бэкэнду не является строго обязательным для Django 1.3, но обеспечивает прямую совместимость настроек.)

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

    SOUTH_TESTS_MIGRATE = False
Etienne
источник
9
Да, точно. Я должен был вставить это в свой ответ! Объедините это с SOUTH_TESTS_MIGRATE = False, и ваши тесты должны быть намного быстрее.
Etienne
7
это является удивительным. в новых настройках django используйте эту строку: 'ENGINE': 'sqlite3' if 'test' в sys.argv else 'django.db.backends.mysql',
mjallday
3
@Tomasz Zielinski - Хм, это зависит от того, что вы тестируете. Но я полностью согласен с тем, что в конце и время от времени вам нужно запускать тесты с вашей реальной базой данных (Postgres, MySQL, Oracle ...). Но выполнение ваших тестов в памяти с помощью sqlite может сэкономить вам много времени.
Этьен
3
Я переворачиваю -1 на +1: как я вижу сейчас, гораздо быстрее использовать sqlite для быстрого запуска и переключаться на MySQL, например, для заключительных ежедневных тестов. (Обратите внимание, что мне пришлось сделать фиктивное редактирование, чтобы разблокировать голосование)
Tomasz Zieliński
12
Осторожно с этим "test" in sys.argv; он может срабатывать, когда вы этого не хотите, например manage.py collectstatic -i test. sys.argv[1] == "test"это более точное условие, которое не должно иметь такой проблемы.
keturn
83

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

python manage.py test --settings=mysite.test_settings myapp

У этого есть два преимущества:

  1. Вам не нужно проверять какое- testлибо волшебное слово в sys.argv, test_settings.pyможно просто

    from settings import *
    
    # make tests faster
    SOUTH_TESTS_MIGRATE = False
    DATABASES['default'] = {'ENGINE': 'django.db.backends.sqlite3'}
    

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

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

    python manage.py test --settings=mysite.test_settings myapp

    и перед фиксацией кода запустить один раз

    python manage.py test myapp

    просто чтобы убедиться, что все тесты действительно проходят.

Анураг Униял
источник
2
Мне нравится такой подход. У меня есть куча разных файлов настроек, и я использую их для разных серверных сред, но я не думал об использовании этого метода для выбора другой тестовой базы данных. Спасибо за идею.
Alexis Bellido
Привет, Anurag, я пробовал это, но другие мои базы данных, упомянутые в настройках, также выполняются. Я не могу понять точную причину.
Bhupesh Pant
Хороший ответ. Интересно, как указать файл настроек при запуске тестов через покрытие.
Wtower
Это хороший подход, но не СУХОЙ. Django уже знает, что вы запускаете тесты. Если бы вы могли каким-то образом «привязаться» к этим знаниям, вы бы были настроены. К сожалению, я считаю, что это требует расширения команды управления. Вероятно, было бы разумно сделать этот общий элемент в ядре фреймворка, например, установив параметр MANAGEMENT_COMMAND на текущую команду всякий раз, когда вызывается manage.py, или что-то в этом роде.
DylanYoung
2
@DylanYoung, вы можете сделать его сухим, включив основные настройки в test_settings и просто переопределив то, что вы хотите для тестирования.
Anurag Uniyal
22

MySQL поддерживает механизм хранения под названием «MEMORY», который вы можете настроить в своей базе данных config ( settings.py) как таковой:

    'USER': 'root',                      # Not used with sqlite3.
    'PASSWORD': '',                  # Not used with sqlite3.
    'OPTIONS': {
        "init_command": "SET storage_engine=MEMORY",
    }

Обратите внимание, что механизм хранения MEMORY не поддерживает столбцы blob / text, поэтому, если вы используете его, django.db.models.TextFieldэто не сработает для вас.

muudscope
источник
5
+1 за упоминание об отсутствии поддержки столбцов blob / text. Он также, похоже, не поддерживает транзакции ( dev.mysql.com/doc/refman/5.6/en/memory-storage-engine.html ).
Туукка Мустонен
Если вам действительно нужны тесты в памяти, вам, вероятно, лучше использовать sqlite, который хотя бы поддерживает транзакции.
atomic77
15

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

Во-первых, убедитесь, что ваша база данных MySQL настроена для использования InnoDB. Затем он может использовать транзакции для отката состояния базы данных перед каждым тестом, что, по моему опыту, привело к значительному ускорению. Вы можете передать команду инициализации базы данных в файле settings.py (синтаксис Django 1.2):

DATABASES = {
    'default': {
            'ENGINE':'django.db.backends.mysql',
            'HOST':'localhost',
            'NAME':'mydb',
            'USER':'whoever',
            'PASSWORD':'whatever',
            'OPTIONS':{"init_command": "SET storage_engine=INNODB" } 
        }
    }

Во-вторых, вам не нужно каждый раз запускать миграции на юг. Задайте его SOUTH_TESTS_MIGRATE = Falseв файле settings.py, и база данных будет создана с помощью обычного syncdb, что будет намного быстрее, чем выполнение всех предыдущих миграций.

Дэниел Розман
источник
Отличный совет! Это сократило мои тесты с 369 tests in 498.704sдо 369 tests in 41.334s . Это более чем в 10 раз быстрее!
Габи Пуркару
Есть ли в settings.py эквивалентный переключатель для миграции в Django 1.7+?
Эдвард Ньюэлл
@EdwardNewell Не совсем так. Но вы можете использовать его --keepдля сохранения базы данных и не требовать повторного применения полного набора миграций при каждом тестовом запуске. Новые миграции по-прежнему будут выполняться. Если вы часто переключаетесь между ветвями, легко попасть в несогласованное состояние (вы можете отменить новые миграции перед переключением, изменив базу данных на тестовую и запустив ее migrate, но это немного неудобно).
DylanYoung
10

Вы можете сделать двойную настройку:

  • использовать транзакционные таблицы: начальное состояние фикстур будет установлено с помощью отката базы данных после каждого TestCase.
  • поместите каталог данных вашей базы данных на ramdisk: вы выиграете много в том, что касается создания базы данных, а также выполнение теста будет быстрее.

Я использую обе уловки и вполне доволен.

Как настроить его для MySQL в Ubuntu:

$ sudo service mysql stop
$ sudo cp -pRL /var/lib/mysql /dev/shm/mysql

$ vim /etc/mysql/my.cnf
# datadir = /dev/shm/mysql
$ sudo service mysql start

Осторожно, это просто для тестирования, после перезагрузки ваша база данных из памяти теряется!

Потр Чачур
источник
Спасибо! работает для меня. Я не могу использовать sqlite, потому что использую функции, специфичные для mysql (полнотекстовые индексы). Для пользователей ubuntu вам нужно будет отредактировать конфигурацию apparmor, чтобы разрешить доступ mysqld к / dev / shm / mysql
Иван Вирабян
Поздравления с Иваном и Потром. На данный момент отключил профиль mysql в AppArmor, но нашел руководство по настройке соответствующего локального профиля: blogs.oracle.com/jsmyth/entry/apparmor_and_mysql
trojjer
Хм. Я пробовал настроить локальный профиль, чтобы предоставить mysqld доступ к пути / dev / shm / mysql и его содержимому, но служба может запускаться только в режиме «жаловаться» (команда aa-жалоба), а не «принудительно», для некоторых причина ... Вопрос к другому форуму! Что я не могу понять, так это то, что вообще нет никаких «жалоб», когда он работает, подразумевая, что mysqld не нарушает профиль ...
Trojjer
4

Другой подход: другой экземпляр MySQL работает в tempfs, который использует RAM Disk. Инструкции в этом сообщении в блоге: Ускорение MySQL для тестирования в Django .

Преимущества:

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

Расширяя ответ Anurag, я упростил процесс, создав те же настройки test_settings и добавив следующее в manage.py

if len(sys.argv) > 1 and sys.argv[1] == "test":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.test_settings")
else:
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")

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

Alvin
источник
2
Осторожно с этим "test" in sys.argv; он может срабатывать, когда вы этого не хотите, например manage.py collectstatic -i test. sys.argv[1] == "test"это более точное условие, которое не должно иметь такой проблемы.
keturn
2
@keturn, таким образом, он генерирует исключение при работе ./manage.pyбез аргументов (например, чтобы увидеть, какие плагины доступны, как и --help)
Энтони Хэтчкинс
1
@AntonyHatchkins Это тривиально решить:len(sys.argv) > 1 and sys.argv[1] == "test"
DylanYoung
1
@DylanYoung Да, это именно то, что я хотел, чтобы Элвин добавил к своему решению, но он не особо заинтересован в его улучшении. В любом случае это больше похоже на быстрый взлом, чем на законное решение.
Энтони Хэтчкинс
1
давно не смотрел на этот ответ, я обновил фрагмент, чтобы отразить улучшение @ DylanYoung
Элвин
0

Используйте ниже в своем setting.py

DATABASES['default']['ENGINE'] = 'django.db.backends.sqlite3'
Эхсан Баркхордар
источник