Las pruebas unitarias de Django tardan mucho tiempo en crear una base de datos de pruebas


Desde hace algún tiempo, mis pruebas unitarias han tardado más de lo esperado. He intentado depurarlo un par de veces sin mucho éxito, ya que los retrasos están antes de que mis pruebas comiencen a ejecutarse. Esto ha afectado mi capacidad de hacer algo remotamente cercano al desarrollo impulsado por pruebas (tal vez mis expectativas son demasiado altas), así que quiero ver si puedo arreglar esto de una vez por todas.

Cuando se ejecuta una prueba, hay un retraso de 70 a 80 segundos entre el inicio y el comienzo real de la prueba. Por ejemplo, si corro una prueba para un módulo pequeño (usando time python manage.py test myapp), obtengo

<... 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

Aproximadamente 1m18 del 1m:21 están entre el

Creating test database for alias 'default'...

Y el

.......

Línea. En otras palabras, la prueba toma menos de 3 segundos, pero la inicialización de la base de datos parece estar tomando 1:18min

Tengo alrededor de 30 aplicaciones, la mayoría con 1 a 3 modelos de base de datos, por lo que esto debería dar una idea del tamaño del proyecto. Utilizo SQLite para pruebas unitarias, y he implementado algunas de las mejoras sugeridas. No puedo publicar todo mi archivo de configuración, pero me complace agregar cualquier información que se requiera.

Yo uso un corredor

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)

Y en mi configuración:

TEST_RUNNER = 'config.test_runner.ExcludeAppsTestSuiteRunner'

También he intentado usar django-nose con django-nose-exclude

He leído mucho sobre cómo acelerar la prueba por sí mismos, pero no he encontrado ninguna pista sobre cómo optimizar o evitar la inicialización de la base de datos. He visto las sugerencias sobre intentar no probar con la base de datos, pero no puedo o no sé cómo evitarlo completamente.

Por favor, hágamelo saber si

  1. Esto es normal y esperado
  2. No se espera (y con suerte una solución o pista sobre qué hacer)

Nuevamente, no necesito ayuda sobre cómo acelerar la prueba en sí, sino la inicialización (o sobrecarga). Quiero que el ejemplo anterior tome 10 segundos en lugar de 80 segundos.

Muchas gracias

Corro la prueba (para una sola aplicación) con --verbose 3 y descubrí que todo esto está relacionado con las migraciones:

  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

I aplastó todas mis migraciones pero sigue siendo lento.

Author: dkarchmer, 2016-04-08

4 answers

La solución final que soluciona mi problema es forzar a Django a deshabilitar la migración durante las pruebas, lo que se puede hacer desde la configuración como esta

TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
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()

O utilizar https://pypi.python.org/pypi/django-test-without-migrations

Toda mi prueba ahora toma alrededor de 1 minuto y una pequeña aplicación tarda 5 segundos.

En mi caso, las migraciones no son necesarias para las pruebas, ya que actualizo las pruebas a medida que migro, y no uso las migraciones para agregar datos. Esto no funcionará para todos

 24
Author: dkarchmer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-11 00:14:26

Resumen

Use pytest !

Operaciones

  1. pip install pytest-django
  2. pytest --nomigrations en lugar de ./manage.py test

Resultado

  • ./manage.py test cuesta 2 min 11.86 seg
  • pytest --nomigrations cuesta 2.18 seg

Consejos

  • Puede crear un archivo llamado pytest.ini en el directorio raíz de su proyecto, y especificar las opciones predeterminadas de la línea de comandos y/o la configuración de Django allí.

    # content of pytest.ini
    [pytest]
    addopts = --nomigrations
    DJANGO_SETTINGS_MODULE = yourproject.settings
    

    Ahora puedes simplemente ejecutar prueba con pytest y te ahorra un poco de escritura.

  • Puede acelerar las pruebas posteriores aún más agregando --reuse-db a las opciones predeterminadas de la línea de comandos.

    [pytest]
    addopts = --nomigrations --reuse-db
    

    Sin embargo, tan pronto como se cambie su modelo de base de datos, debe ejecutar pytest --create-db una vez para forzar la recreación de la base de datos de prueba.

  • Si necesita habilitar gevent monkey patching durante las pruebas, puede crear un archivo llamado pytest en la raíz de su proyecto directorio con el siguiente contenido, envíe el bit de ejecución a él (chmod +x pytest) y ejecute ./pytest para probar en lugar de 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())
    

    Puede crear un archivo test_gevent.py para probar si el parche de gevent monkey es exitoso:

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

Referencias

 19
Author: Rockallite,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-08 08:10:45

Use ./manage.py probar ke keepdb cuando no hay cambios en los archivos de migración

 5
Author: Manoj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-18 13:43:39

La inicialización de la base de datos toma demasiado tiempo...

Tengo un proyecto con aproximadamente el mismo número de modelos/tablas (alrededor de 77), y aproximadamente 350 pruebas y toma 1 minuto en total para ejecutar todo. Deving en una máquina vagabunda con 2 cpu asignadas y 2 GB de ram. También uso py.prueba con el plugin pytest-xdist para ejecutar múltiples pruebas en paralelo.

Otra cosa que puede hacer es decirle a django que reutilice la base de datos de prueba y solo vuelva a crearla cuando tenga cambios de esquema. También tú puede usar SQLite para que las pruebas usen una base de datos en memoria. Ambos enfoques explicados aquí: https://docs.djangoproject.com/en/dev/topics/testing/overview/#the-test-database

EDITAR: En caso de que ninguna de las opciones anteriores funcione, una opción más es hacer que sus pruebas unitarias hereden de django SimpleTestCase o usar un ejecutor de pruebas personalizado que no cree una base de datos como se explica en esta respuesta: pruebas unitarias de django sin una db.

Entonces puedes simplemente simula llamadas de django a la base de datos usando una biblioteca como esta (que admito que escribí): https://github.com/stphivos/django-mock-queries

De esta manera puede ejecutar sus pruebas unitarias localmente rápido y dejar que su servidor CI se preocupe por ejecutar pruebas de integración que requieren una base de datos, antes de fusionar su código en alguna rama estable dev/master que no sea la de producción.

 2
Author: fips,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:44