Django Unit Testing занимает очень много времени для создания тестовой базы данных
В течение некоторого времени мое модульное тестирование длилось более ожидаемого времени. Я пытался отладить его несколько раз без особых успехов, так как задержки до того, как мои тесты даже начнут работать. Это повлияло на мою способность делать что-либо дистанционно близко к разработке, основанному на тестировании (возможно, мои ожидания слишком высоки), поэтому я хочу посмотреть, могу ли я исправить это раз и навсегда.
При запуске теста существует задержка от 70 до 80 секунд между началом и фактическим началом теста. Например, если я запускаю тест для небольшого модуля (используя time python manage.py test myapp
), я получаю
<... bunch of unimportant print messages I print from my settings>
Creating test database for alias 'default'...
......
----------------------------------------------------------------
Ran 6 tests in 2.161s
OK
Destroying test database for alias 'default'...
real 1m21.612s
user 1m17.170s
sys 0m1.400s
Около 1m18 из 1m: 21 находятся между
Creating test database for alias 'default'...
и
.......
линии. Другими словами, тест занимает менее 3 секунд, но инициализация базы данных, по-видимому, занимает 1:18мин
У меня около 30 приложений, большинство из которых имеют от 1 до 3 моделей баз данных, поэтому это должно дать представление о размере проекта. Я использую SQLite для модульного тестирования и реализовал некоторые из предлагаемых улучшений. Я не могу опубликовать весь файл настроек, но с удовольствием добавляю любую информацию, которая требуется.
Я использую бегун
from django.test.runner import DiscoverRunner
from django.conf import settings
class ExcludeAppsTestSuiteRunner(DiscoverRunner):
"""Override the default django 'test' command, exclude from testing
apps which we know will fail."""
def run_tests(self, test_labels, extra_tests=None, **kwargs):
if not test_labels:
# No appnames specified on the command line, so we run all
# tests, but remove those which we know are troublesome.
test_labels = (
'app1',
'app2',
....
)
print ('Testing: ' + str(test_labels))
return super(ExcludeAppsTestSuiteRunner, self).run_tests(
test_labels, extra_tests, **kwargs)
и в моих настройках:
TEST_RUNNER = 'config.test_runner.ExcludeAppsTestSuiteRunner'
Я также пробовал использовать django-nose
с django-nose-exclude
Я много читал о том, как ускорить сам тест, но не нашел никаких указаний о том, как оптимизировать или избежать инициализации базы данных. Я видел предложения по попытке не тестировать базу данных, но я не могу или не знаю, как полностью избежать этого.
Пожалуйста, дайте мне знать, если
- Это нормально и ожидается
- Не ожидается (и, надеюсь, исправить или привести к тому, что делать)
Опять же, мне не нужна помощь в том, как ускорить сам тест, но инициализировать (или накладные расходы). Я хочу, чтобы приведенный выше пример выполнял 10 секунд вместо 80 секунд.
Большое спасибо
Я запускаю тест (для одного приложения) с помощью --verbose 3
и обнаружил, что все это связано с миграциями:
Rendering model states... DONE (40.500s)
Applying authentication.0001_initial... OK (0.005s)
Applying account.0001_initial... OK (0.022s)
Applying account.0002_email_max_length... OK (0.016s)
Applying contenttypes.0001_initial... OK (0.024s)
Applying contenttypes.0002_remove_content_type_name... OK (0.048s)
Applying s3video.0001_initial... OK (0.021s)
Applying s3picture.0001_initial... OK (0.052s)
... Many more like this
Я раздавил все мои миграции, но все еще медленно.
Ответы
Ответ 1
Окончательное решение, которое решает мою проблему, состоит в том, чтобы заставить Django отключить миграцию во время тестирования, что можно сделать из настроек, подобных этой
TESTING = 'test' in sys.argv[1:]
if TESTING:
print('=========================')
print('In TEST Mode - Disableling Migrations')
print('=========================')
class DisableMigrations(object):
def __contains__(self, item):
return True
def __getitem__(self, item):
return "notmigrations"
MIGRATION_MODULES = DisableMigrations()
или используйте https://pypi.python.org/pypi/django-test-without-migrations
Весь мой тест теперь занимает около 1 минуты, а маленькое приложение - 5 секунд.
В моем случае миграции не нужны для тестирования, так как я обновляю тесты во время миграции и не использую миграции для добавления данных. Это не будет работать для всех
Ответ 2
Резюме
Используйте pytest
!
Операции
-
pip install pytest-django
-
pytest --nomigrations
вместо ./manage.py test
Результат
-
./manage.py test
стоит 2 мин. 11,86 сек.
-
pytest --nomigrations
стоит 2,18 секунды
Советы
-
Вы можете создать файл с именем pytest.ini
в корневом каталоге проекта и указать параметры командной строки по умолчанию и/или настройки Django.
# content of pytest.ini
[pytest]
addopts = --nomigrations
DJANGO_SETTINGS_MODULE = yourproject.settings
Теперь вы можете просто запускать тесты с помощью pytest
и немного печатать.
-
Вы также можете ускорить последующие тесты, добавив --reuse-db
к параметрам командной строки по умолчанию.
[pytest]
addopts = --nomigrations --reuse-db
Однако, как только ваша модель базы данных будет изменена, вы должны запустить pytest --create-db
один раз, чтобы принудительное повторное создание тестовой базы данных.
-
Если вам нужно включить gevent monkey patching во время тестирования, вы можете создать файл с именем pytest
в корневом каталоге проекта со следующим содержимым, приложите к нему бит выполнения (chmod +x pytest
) и запустите ./pytest
для тестирования вместо pytest
:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# content of pytest
from gevent import monkey
monkey.patch_all()
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "yourproject.settings")
from django.db import connection
connection.allow_thread_sharing = True
import re
import sys
from pytest import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
Вы можете создать файл test_gevent.py
для проверки успешности исправления обезьяны gevent:
# -*- coding: utf-8 -*-
# content of test_gevent.py
import time
from django.test import TestCase
from django.db import connection
import gevent
def f(n):
cur = connection.cursor()
cur.execute("SELECT SLEEP(%s)", (n,))
cur.execute("SELECT %s", (n,))
cur.fetchall()
connection.close()
class GeventTestCase(TestCase):
longMessage = True
def test_gevent_spawn(self):
timer = time.time()
d1, d2, d3 = 1, 2, 3
t1 = gevent.spawn(f, d1)
t2 = gevent.spawn(f, d2)
t3 = gevent.spawn(f, d3)
gevent.joinall([t1, t2, t3])
cost = time.time() - timer
self.assertAlmostEqual(cost, max(d1, d2, d3), delta=1.0,
msg='gevent spawn not working as expected')
Ссылки
Ответ 3
используйте тест./manage.py --keepdb, если в файлах миграции нет изменений
Ответ 4
Инициализация базы данных действительно занимает слишком много времени...
У меня есть проект с примерно таким же количеством моделей/таблиц (около 77) и примерно 350 тестов и занимает 1 минуту, чтобы запустить все. Девять в бродячей машине с выделенным 2 cpus и 2 ГБ оперативной памяти. Также я использую py.test с плагином pytest-xdist для параллельной параллельной работы нескольких тестов.
Еще одна вещь, которую вы можете сделать, это сказать, что django повторно использует тестовую базу данных и только повторно создает ее при изменении схемы. Также вы можете использовать SQLite, чтобы тесты использовали базу данных в памяти. Оба подхода объясняются здесь:
https://docs.djangoproject.com/en/dev/topics/testing/overview/#the-test-database
РЕДАКТИРОВАТЬ. Если ни одна из вышеперечисленных опций не работает, еще один вариант заключается в том, чтобы ваши модульные тесты наследовали от django SimpleTestCase или использовали пользовательский тестовый бегун, который не создает базу данных, как описано в этот ответ здесь: модульные тесты django без db.
Затем вы можете просто высмеять вызовы django в базу данных, используя библиотеку, подобную этой (которая, как я понимаю): https://github.com/stphivos/django-mock-queries
Таким образом, вы можете быстро запускать свои модульные тесты и позволить вашему CI-серверу беспокоиться о выполнении интеграционных тестов, требующих базы данных, до слияния вашего кода с какой-то стабильной ветвью dev/master, которая не является производственной.