Модульное тестирование Django требует очень много времени для создания тестовой базы данных

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

При запуске теста существует задержка от 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

Около 1:18 из 1: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

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

Пожалуйста, дайте мне знать, если

  1. Это нормально и ожидаемо
  2. Не ожидается (и, надеюсь, исправление или указание на то, что делать)

Опять же, мне нужна помощь не в том, как ускорить сам тест, а в инициализации (или накладных расходах). Я хочу, чтобы приведенный выше пример занимал 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

Я раздавил все свои миграции, но все еще медленно.


person dkarchmer    schedule 07.04.2016    source источник
comment
Очень полезно. Особенно вариант verbose. В нашем проекте есть пара сотен миграций, некоторые из которых, по-видимому, занимают до секунды. Для тех, кто использует PyCharm (Pro), вы можете добавить --verbose 3 (или -v 3) в конфигурацию запуска для своего теста (в разделе «Параметры:»).   -  person djvg    schedule 13.12.2018


Ответы (4)


Окончательное решение, которое устраняет мою проблему, — заставить 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 None

    MIGRATION_MODULES = DisableMigrations()

или используйте https://pypi.python.org/pypi/django-test-without-migrations

Весь мой тест теперь занимает около 1 минуты, а небольшое приложение - 5 секунд.

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

person dkarchmer    schedule 11.05.2016
comment
Круто, у вас должно быть много миграций! :-) - person fips; 12.05.2016
comment
Не совсем так, как я раздавил все свои. Но у меня есть проблема (для другого поста), когда Django-allauth продолжает создавать новую миграцию каждый раз, когда я делаю миграции. Но есть много сообщений о медленных миграциях Django 1.8. Я считаю, что 1.9 исправила некоторые из них. - person dkarchmer; 12.05.2016
comment
Да, действительно, я где-то читал об этом. Чтобы решить эту проблему, я просто использую pytest-django, у которого есть опция --nomigrations, которая создает базу данных напрямую из моделей. По-видимому, это было поведением Django по умолчанию до версии 1.6. Я знаю, что полезно запускать миграцию один раз перед развертыванием, но не при каждом тестовом прогоне! - person fips; 12.05.2016
comment
разве --keepdb не сделает то же самое? - person Tomek; 11.11.2016
comment
Это было некоторое время назад, поэтому я не помню подробностей, но я помню, что даже при выполнении одного теста была дополнительная задержка, поэтому сохранение базы данных, насколько я понимаю, не помогло бы с этим. Несмотря на это, я не хочу иметь дело с какой-то несинхронизированной базой данных, поэтому приведенное ниже решение отлично сработало для меня. - person dkarchmer; 11.11.2016
comment
Это верно, --keepdb не помогает решить проблему медленной миграции. Это решение делает. Спасибо! - person slumtrimpet; 07.08.2017
comment
С django 2.1 мне пришлось изменить return "notmigrations" на return None, иначе он жаловался, что ModuleNotFoundError: No module named 'notmigrations' - person frnhr; 11.06.2019

Резюме

Используйте 1_ !

Операции

  1. pip install pytest-django
  2. 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 во время тестирования, вы можете создать файл с именем 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 monkey:

    # -*- 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')
    

Ссылки

person Rockallite    schedule 30.09.2016
comment
Кроме того, если вы устали весь день смотреть на маленькие серые точки, pip install pytest-sugar и вдруг тестирование - это красиво! - person Daniel Quinn; 06.12.2016
comment
Я пробовал этот подход, но получил ошибку относительно разрешений БД. Я думаю, что мне нужно обернуть все мои тестовые примеры декоратором, чтобы это сработало. pytest-django.readthedocs.io/ ru/последние/. Но это кажется слишком навязчивым. - person Crystal; 16.01.2018

используйте ./manage.py test --keepdb когда в файлах миграции нет изменений

person Manoj    schedule 18.09.2017
comment
Есть ли потенциальные подводные камни в этом подходе? - person Simen Russnes; 29.07.2020
comment
Меня беспокоит только одно: будет ли очищаться база данных после каждого запуска тестового примера? но в остальном вы экономите много времени на выполнении запросов DDL SQL при каждой миграции. Это особенно медленно, когда у вас нет SSD. Это также должно ускориться, если у вас, скажем, 100 миграций. Это временное исправление для быстрого запуска тестов один за другим во время разработки без ожидания 5-10 минут между каждым. - person Manoj; 19.08.2020

Инициализация базы данных действительно занимает слишком много времени...

У меня есть проект с примерно таким же количеством моделей/таблиц (около 77) и примерно 350 тестов, и для запуска всего требуется 1 минута. Разработка на бродячей машине с 2 выделенными процессорами и 2 ГБ оперативной памяти. Также я использую py.test с плагином pytest-xdist для параллельного запуска нескольких тестов.

Еще одна вещь, которую вы можете сделать, это указать django повторно использовать тестовую базу данных и воссоздавать ее только при изменении схемы. Также вы можете использовать SQLite, чтобы тесты использовали базу данных в памяти. Оба подхода объясняются здесь: https://docs.djangoproject.com/en/dev/topics/testing/overview/#the-test-database

РЕДАКТИРОВАТЬ: если ни один из вышеперечисленных вариантов не работает, еще один вариант — наследовать ваши модульные тесты от django SimpleTestCase или использовать пользовательский инструмент запуска тестов, который не создает базу данных, как описано в этом ответе здесь. : модульные тесты django без базы данных.

Затем вы можете просто имитировать вызовы django к базе данных, используя библиотеку, подобную этой (которую я, по общему признанию, написал): https://github.com/stphivos/django-mock-queries

Таким образом, вы можете быстро запускать свои модульные тесты локально и позволить своему CI-серверу беспокоиться о выполнении интеграционных тестов, для которых требуется база данных, прежде чем объединять свой код с какой-либо стабильной веткой dev/master, которая не является производственной.

person fips    schedule 07.04.2016
comment
Спасибо за ответ. Я только что исправил OP, чтобы указать, что я уже использую SQLite в памяти. Я только что попробовал --keepdb, но, похоже, не имеет никакого эффекта, возможно, потому, что я использую SQLite в памяти. Есть ли способ использовать SQLite, но с файлом базы данных, чтобы его можно было сохранить? Мой общий тест (174 теста) выполняется с инициализацией 1:18 плюс 1 мин = 2:18 мин, так что это приемлемо. В вашем случае ваша инициализация явно намного меньше. Я не думаю, что инициализацию базы данных можно выполнять параллельно, поэтому кажется, что у вас нет накладных расходов. Так ли это? - person dkarchmer; 08.04.2016
comment
В целом, я бы сказал, что инициализация базы данных будет более интенсивной при вводе-выводе, а выполнение тестов требует больше памяти и процессора, особенно для распределенного тестирования. Но вы используете базу данных SQLite в памяти, поэтому не знаете, сколько операций ввода-вывода требуется для инициализации базы данных. Никогда еще не было так много приложений в одном проекте, не знаю, сколько накладных расходов требуется для обнаружения моделей в каждом из них. Ваши характеристики? Также вы создаете большое количество тестовых данных перед запуском тестов? - person fips; 08.04.2016
comment
Кажется, у нас примерно одинаковое количество моделей, плюс-минус, и у вас больше тестов, чем у меня, поэтому я надеюсь, что смогу получить ваши результаты. Я не знаю, что вы имеете в виду под спецификацией, но нет, у меня нет приспособлений. Все данные создаются как часть самого теста или во время setUp() - person dkarchmer; 08.04.2016
comment
Извините, я хотел спросить вас, имеет ли ваша машина для разработки ограниченные характеристики. Обновил мой ответ решением для этого. - person fips; 08.04.2016
comment
Нет, я ничего не пробовал, включая раздавливание всех попыток миграции. Мне нужно попробовать насмешливую библиотеку, но я не смог этого сделать - person dkarchmer; 07.05.2016
comment
Добавлены некоторые примечания при запуске теста с параметром --verbose 3, который показывает, что все это связано со временем миграции при создании базы данных. - person dkarchmer; 11.05.2016