¿Cómo ejecutar la base de datos de prueba de Django solo en la memoria?

125

Mis pruebas unitarias de Django tardan mucho tiempo en ejecutarse, así que estoy buscando formas de acelerarlo. Estoy considerando instalar un SSD , pero sé que también tiene sus desventajas. Por supuesto, hay cosas que podría hacer con mi código, pero estoy buscando una solución estructural. Incluso ejecutar una sola prueba es lento ya que la base de datos necesita ser reconstruida / migrada al sur cada vez. Así que aquí está mi idea ...

Como sé que la base de datos de prueba siempre será bastante pequeña, ¿por qué no puedo configurar el sistema para mantener siempre toda la base de datos de prueba en RAM? Nunca toque el disco en absoluto. ¿Cómo configuro esto en Django? Prefiero seguir usando MySQL, ya que eso es lo que uso en la producción, pero si SQLite  3 u otra cosa lo hace fácil, seguiría ese camino.

¿SQLite o MySQL tienen una opción para ejecutarse completamente en la memoria? Debería ser posible configurar un disco RAM y luego configurar la base de datos de prueba para almacenar sus datos allí, pero no estoy seguro de cómo decirle a Django / MySQL que use un directorio de datos diferente para una determinada base de datos, especialmente porque se sigue borrando y recreó cada carrera. (Estoy en una Mac FWIW).

Leopd
fuente

Respuestas:

164

Si configura su motor de base de datos en sqlite3 cuando ejecuta sus pruebas, Django usará una base de datos en memoria .

Estoy usando un código como este en mi settings.pypara configurar el motor en sqlite cuando ejecuto mis pruebas:

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

O en Django 1.2:

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

Y finalmente en Django 1.3 y 1.4:

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

(La ruta completa al backend no es estrictamente necesaria con Django 1.3, pero hace que la configuración sea compatible).

También puede agregar la siguiente línea, en caso de que tenga problemas con las migraciones del sur:

    SOUTH_TESTS_MIGRATE = False
Etienne
fuente
9
Sí exactamente. ¡Debería haber puesto eso en mi respuesta! Combine eso con SOUTH_TESTS_MIGRATE = False y sus pruebas deberían ser mucho más rápidas.
Etienne
77
Esto es asombroso. en las configuraciones más recientes de django, use esta línea: 'ENGINE': 'sqlite3' if 'test' en sys.argv más 'django.db.backends.mysql',
mjallday
3
@Tomasz Zielinski - Hmm, depende de lo que estés probando. Pero estoy totalmente de acuerdo en que, al final y de vez en cuando, debe ejecutar las pruebas con su base de datos real (Postgres, MySQL, Oracle ...). Pero ejecutar sus pruebas en memoria con sqlite puede ahorrarle mucho tiempo.
Etienne
3
Invierto -1 a +1: como lo veo ahora, es mucho más rápido usar sqlite para ejecuciones rápidas y cambiar a MySQL, por ejemplo, para las pruebas diarias finales. (Tenga en cuenta que tuve que hacer una edición ficticia para desbloquear la votación)
Tomasz Zieliński
12
Precaución con esto "test" in sys.argv; puede activarse cuando no lo desee, por ejemplo manage.py collectstatic -i test. sys.argv[1] == "test"Es una condición más precisa que no debería tener ese problema.
keturn
83

Por lo general, creo un archivo de configuración separado para las pruebas y lo uso en el comando de prueba, por ejemplo

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

Tiene dos beneficios:

  1. No tiene que buscar testni ninguna palabra mágica en sys.argv, test_settings.pysimplemente puede ser

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

    O bien, puede ajustarlo según sus necesidades, separando limpiamente la configuración de prueba de la configuración de producción.

  2. Otro beneficio es que puede ejecutar la prueba con el motor de base de datos de producción en lugar de sqlite3 evitando errores sutiles, por lo que al desarrollar el uso

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

    y antes de comprometer el código, ejecutar una vez

    python manage.py test myapp

    solo para estar seguro de que todas las pruebas realmente están pasando.

Anurag Uniyal
fuente
2
Me gusta este enfoque. Tengo un montón de archivos de configuración diferentes y los uso para diferentes entornos de servidor, pero no había pensado en usar este método para elegir una base de datos de prueba diferente. Gracias por la idea
Alexis Bellido
Hola Anurag, probé esto pero mis otras bases de datos mencionadas en la configuración también se ejecutan. No puedo averiguar la razón exacta.
Bhupesh Pant
Buena respuesta. Me pregunto cómo especificar un archivo de configuración al ejecutar pruebas a través de la cobertura.
Wtower
Es un buen enfoque, pero no SECO. Django ya sabe que estás ejecutando pruebas. Si pudieras 'enganchar' este conocimiento de alguna manera, estarías listo. Desafortunadamente, creo que eso requiere extender el comando de administración. Probablemente tendría sentido hacer esto genérico en el núcleo del marco, por ejemplo, teniendo una configuración MANAGEMENT_COMMAND establecida en el comando actual siempre que se llame a manage.py, o algo por el estilo.
DylanYoung
2
@DylanYoung puede dejarlo seco al incluir la configuración principal en test_settings y simplemente anular las cosas que desea para la prueba.
Anurag Uniyal
22

MySQL admite un motor de almacenamiento llamado "MEMORY", que puede configurar en la configuración de su base de datos ( settings.py) como tal:

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

Tenga en cuenta que el motor de almacenamiento MEMORY no admite columnas de blob / texto, por lo que si está usando django.db.models.TextFieldesto no funcionará para usted.

muudscope
fuente
55
+1 por mencionar la falta de soporte para las columnas blob / text. Tampoco parece admitir transacciones ( dev.mysql.com/doc/refman/5.6/en/memory-storage-engine.html ).
Tuukka Mustonen
Si realmente desea pruebas en memoria, probablemente sea mejor que use sqlite, que al menos admite transacciones.
atomic77 el
15

No puedo responder a su pregunta principal, pero hay algunas cosas que puede hacer para acelerar las cosas.

En primer lugar, asegúrese de que su base de datos MySQL esté configurada para usar InnoDB. Luego puede usar transacciones para revertir el estado de la base de datos antes de cada prueba, lo que en mi experiencia ha llevado a una aceleración masiva. Puede pasar un comando de inicio de base de datos en su settings.py (sintaxis de 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" } 
        }
    }

En segundo lugar, no necesita ejecutar las migraciones del sur cada vez. Establezca SOUTH_TESTS_MIGRATE = Falseen settings.py y la base de datos se creará con syncdb simple, que será mucho más rápido que ejecutar todas las migraciones históricas.

Daniel Roseman
fuente
Gran consejo! Redujo mis pruebas de 369 tests in 498.704sa 369 tests in 41.334s . ¡Esto es más de 10 veces más rápido!
Gabi Purcaru
¿Hay un interruptor equivalente en settings.py para migraciones en Django 1.7+?
Edward Newell
@EdwardNewell No exactamente. Pero puede usar --keeppara conservar la base de datos y no requerir que se vuelva a aplicar su conjunto completo de migraciones en cada ejecución de prueba. Nuevas migraciones aún se ejecutarán. Sin embargo, si cambia de sucursal con frecuencia, es fácil entrar en un estado inconsistente (puede revertir nuevas migraciones antes de cambiar cambiando la base de datos a la base de datos de prueba y ejecutándose migrate, pero es un poco difícil).
DylanYoung
10

Puedes hacer ajustes dobles:

  • use tablas transaccionales: el estado de los dispositivos iniciales se establecerá utilizando la reversión de la base de datos después de cada TestCase.
  • ponga el directorio de datos de su base de datos en ramdisk: obtendrá mucho en lo que respecta a la creación de la base de datos y también ejecutar la prueba será más rápido.

Estoy usando ambos trucos y estoy bastante feliz.

Cómo configurarlo para MySQL en 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

¡Cuidado, es solo para probar, después de reiniciar su base de datos desde la memoria se pierde!

Potr Czachur
fuente
¡Gracias! funciona para mi. No puedo usar sqlite, porque estoy usando características específicas de mysql (índices de texto completo). Para los usuarios de ubuntu, deberá editar su configuración de apparmor para permitir el acceso de mysqld a / dev / shm / mysql
Ivan Virabyan
Saludos para los jefes de Ivan y Potr. Inhabilité el perfil de MySQL de AppArmor por ahora, pero encontré una guía para personalizar el perfil local relevante: blogs.oracle.com/jsmyth/entry/apparmor_and_mysql
trojjer
Hmm Intenté personalizar el perfil local para dar acceso mysqld a la ruta / dev / shm / mysql y sus contenidos, pero el servicio solo puede comenzar en modo 'quejarse' (comando aa-quejarse) y no 'forzar', para algunos razón ... ¡Una pregunta para otro foro! Lo que no puedo entender es cómo no hay 'quejas' en absoluto cuando funciona, lo que implica que mysqld no está violando el perfil ...
trojjer
4

Otro enfoque: tener otra instancia de MySQL ejecutándose en un tempfs que use un disco RAM. Instrucciones en esta publicación de blog: Acelerar MySQL para probar en Django .

Ventajas:

  • Usas exactamente la misma base de datos que usa tu servidor de producción
  • no es necesario cambiar su configuración predeterminada de mysql
neves
fuente
2

Ampliando la respuesta de Anurag, simplifiqué el proceso creando los mismos test_settings y agregando lo siguiente a 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")

parece más limpio ya que sys ya se importó y manage.py solo se usa a través de la línea de comandos, por lo que no es necesario saturar la configuración

Alvin
fuente
2
Precaución con esto "test" in sys.argv; puede activarse cuando no lo desee, por ejemplo manage.py collectstatic -i test. sys.argv[1] == "test"Es una condición más precisa que no debería tener ese problema.
keturn
2
@keturn de esta manera genera una excepción cuando se ejecuta ./manage.pysin argumentos (por ejemplo, para ver qué complementos están disponibles, igual que --help)
Antony Hatchkins
1
@AntonyHatchkins Eso es trivial de resolver:len(sys.argv) > 1 and sys.argv[1] == "test"
DylanYoung
1
@DylanYoung Sí, eso es exactamente lo que quería que Alvin agregara a su solución, pero no está particularmente interesado en mejorarla. De todos modos, parece más un truco rápido que la solución legítima.
Antony Hatchkins
1
No he visto esta respuesta en mucho tiempo, actualicé el fragmento para reflejar la mejora de @ DylanYoung
Alvin
0

Use a continuación en su setting.py

DATABASES['default']['ENGINE'] = 'django.db.backends.sqlite3'
Ehsan Barkhordar
fuente