Comment exécuter la base de données de test de Django uniquement en mémoire?

125

Mes tests unitaires Django prennent beaucoup de temps à s'exécuter, je cherche donc des moyens d'accélérer cela. J'envisage d'installer un SSD , mais je sais que cela a aussi ses inconvénients. Bien sûr, il y a des choses que je pourrais faire avec mon code, mais je recherche une solution structurelle. Même l'exécution d'un seul test est lente car la base de données doit être reconstruite / migrée vers le sud à chaque fois. Alors voici mon idée ...

Puisque je sais que la base de données de test sera toujours assez petite, pourquoi ne puis-je pas simplement configurer le système pour toujours garder toute la base de données de test dans la RAM? Ne touchez jamais du tout au disque. Comment configurer cela dans Django? Je préférerais continuer à utiliser MySQL car c'est ce que j'utilise en production, mais si SQLite  3 ou autre chose rend cela facile, j'irais dans cette direction.

SQLite ou MySQL a-t-il une option pour s'exécuter entièrement en mémoire? Il devrait être possible de configurer un disque RAM, puis de configurer la base de données de test pour y stocker ses données, mais je ne sais pas comment dire à Django / MySQL d'utiliser un répertoire de données différent pour une certaine base de données, d'autant plus qu'elle ne cesse de s'effacer et recréé chaque essai. (Je suis sur un Mac FWIW.)

Léopd
la source

Réponses:

164

Si vous définissez votre moteur de base de données sur sqlite3 lorsque vous exécutez vos tests, Django utilisera une base de données en mémoire .

J'utilise un code comme celui-ci dans mon settings.pypour définir le moteur sur sqlite lors de l'exécution de mes tests:

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

Ou dans Django 1.2:

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

Et enfin dans Django 1.3 et 1.4:

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

(Le chemin complet vers le backend n'est pas strictement nécessaire avec Django 1.3, mais rend le paramétrage en avant compatible.)

Vous pouvez également ajouter la ligne suivante, au cas où vous rencontriez des problèmes avec les migrations Sud:

    SOUTH_TESTS_MIGRATE = False
Etienne
la source
9
Oui, exactement. J'aurais dû mettre cela dans ma réponse! Combinez cela avec SOUTH_TESTS_MIGRATE = False et vos tests devraient être beaucoup plus rapides.
Etienne
7
c'est génial. sur les nouvelles configurations django, utilisez cette ligne: 'ENGINE': 'sqlite3' if 'test' in sys.argv else 'django.db.backends.mysql',
mjallday
3
@Tomasz Zielinski - Hmm, cela dépend de ce que vous testez. Mais je suis tout à fait d'accord que, à la fin et de temps en temps, vous devez exécuter les tests avec votre vraie base de données (Postgres, MySQL, Oracle ...). Mais exécuter vos tests en mémoire avec sqlite peut vous faire gagner beaucoup de temps.
Etienne
3
J'inverse -1 à +1: comme je le vois maintenant, il est beaucoup plus rapide d'utiliser sqlite pour des exécutions rapides et de passer à MySQL pour les tests quotidiens finaux par exemple. (Notez que j'ai dû faire une modification factice pour déverrouiller le vote)
Tomasz Zieliński
12
Attention à cela "test" in sys.argv; il peut se déclencher lorsque vous ne le souhaitez pas, par exemple manage.py collectstatic -i test. sys.argv[1] == "test"est une condition plus précise qui ne devrait pas avoir ce problème.
retour le
83

Je crée généralement un fichier de paramètres séparé pour les tests et je l'utilise dans la commande de test, par exemple

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

Il présente deux avantages:

  1. Vous n'avez pas à vérifier testou un tel mot magique dans sys.argv, test_settings.pypeut simplement être

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

    Vous pouvez également l'ajuster davantage à vos besoins, en séparant proprement les paramètres de test des paramètres de production.

  2. Un autre avantage est que vous pouvez exécuter des tests avec le moteur de base de données de production au lieu de sqlite3 en évitant les bogues subtils, donc lors du développement de l'utilisation

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

    et avant de valider le code, exécutez une fois

    python manage.py test myapp

    juste pour être sûr que tous les tests réussissent vraiment.

Anurag Uniyal
la source
2
J'aime cette approche. J'ai un tas de fichiers de paramètres différents et je les utilise pour différents environnements de serveur mais je n'avais pas pensé à utiliser cette méthode pour choisir une base de données de test différente. Merci pour l'idée.
Alexis Bellido
Salut Anurag, j'ai essayé cela mais mes autres bases de données mentionnées dans les paramètres sont également exécutées. Je ne parviens pas à comprendre la raison exacte.
Bhupesh Pant
Bonne réponse. Je me demande comment spécifier un fichier de paramètres lors de l'exécution de tests via la couverture.
Wtower
C'est une bonne approche, mais pas DRY. Django sait déjà que vous exécutez des tests. Si vous pouviez vous «accrocher» à cette connaissance d'une manière ou d'une autre, vous seriez prêt. Malheureusement, je pense que cela nécessite d'étendre la commande de gestion. Il serait probablement judicieux de rendre ce générique au cœur du framework, par exemple en ayant un paramètre MANAGEMENT_COMMAND défini sur la commande actuelle chaque fois que manage.py est appelé, ou quelque chose dans ce sens.
DylanYoung
2
@DylanYoung, vous pouvez le rendre sec en incluant les paramètres principaux dans test_settings et en remplaçant simplement les éléments que vous souhaitez tester.
Anurag Uniyal
22

MySQL prend en charge un moteur de stockage appelé "MEMORY", que vous pouvez configurer dans votre base de données config ( settings.py) comme tel:

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

Notez que le moteur de stockage MEMORY ne prend pas en charge les colonnes blob / texte, donc si vous l'utilisez, django.db.models.TextFieldcela ne fonctionnera pas pour vous.

muudscope
la source
5
+1 pour avoir mentionné le manque de prise en charge des colonnes blob / texte. Il ne semble pas non plus prendre en charge les transactions ( dev.mysql.com/doc/refman/5.6/en/memory-storage-engine.html ).
Tuukka Mustonen
Si vous voulez vraiment des tests en mémoire, vous feriez probablement mieux d'utiliser sqlite qui prend au moins en charge les transactions.
atomic77
15

Je ne peux pas répondre à votre question principale, mais il y a plusieurs choses que vous pouvez faire pour accélérer les choses.

Tout d'abord, assurez-vous que votre base de données MySQL est configurée pour utiliser InnoDB. Ensuite, il peut utiliser des transactions pour restaurer l'état de la base de données avant chaque test, ce qui, selon mon expérience, a conduit à une accélération massive. Vous pouvez passer une commande d'initialisation de la base de données dans votre settings.py (syntaxe 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" } 
        }
    }

Deuxièmement, vous n'avez pas besoin d'exécuter les migrations Sud à chaque fois. Définissez SOUTH_TESTS_MIGRATE = Falsedans votre settings.py et la base de données sera créée avec syncdb simple, ce qui sera beaucoup plus rapide que d'exécuter toutes les migrations historiques.

Daniel Roseman
la source
Bon conseil! Cela a réduit mes tests de 369 tests in 498.704sà 369 tests in 41.334s . C'est plus de 10 fois plus rapide!
Gabi Purcaru le
Existe-t-il un commutateur équivalent dans settings.py pour les migrations dans Django 1.7+?
Edward Newell
@EdwardNewell Pas exactement. Mais vous pouvez utiliser --keeppour conserver la base de données et ne pas exiger que votre ensemble complet de migrations soit réappliqué à chaque exécution de test. Les nouvelles migrations continueront de fonctionner. Si vous passez fréquemment d'une branche à l'autre, il est cependant facile d'entrer dans un état incohérent (vous pouvez annuler les nouvelles migrations avant de passer en changeant la base de données vers la base de données de test et en l'exécutant migrate, mais c'est un peu pénible).
DylanYoung
10

Vous pouvez faire un double ajustement:

  • utiliser des tables transactionnelles: l'état initial des fixtures sera défini à l'aide de la restauration de la base de données après chaque TestCase.
  • mettez votre répertoire de données de base de données sur le disque virtuel: vous gagnerez beaucoup en ce qui concerne la création de la base de données et l'exécution du test sera plus rapide.

J'utilise les deux trucs et je suis plutôt content.

Comment le configurer pour MySQL sur 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

Attention, c'est juste pour le test, après le redémarrage de votre base de données depuis la mémoire est perdue!

Potr Czachur
la source
Merci! travaille pour moi. Je ne peux pas utiliser sqlite, car j'utilise des fonctionnalités spécifiques à mysql (index de texte intégral). Pour les utilisateurs d'ubuntu, vous devrez modifier votre configuration apparmor pour autoriser l'accès mysqld à / dev / shm / mysql
Ivan Virabyan
Bravo pour le heads-up Ivan et Potr. Désactivé le profil mysql AppArmor pour le moment, mais
j'ai
Hmm. J'ai essayé de personnaliser le profil local pour donner à mysqld l'accès au chemin / dev / shm / mysql et à son contenu, mais le service ne peut démarrer qu'en mode `` se plaindre '' (commande aa-plaint) et non `` appliquer '', pour certains raison ... Une question pour un autre forum! Ce que je ne peux pas comprendre, c'est qu'il n'y a pas de `` plaintes '' du tout quand cela fonctionne, ce qui implique que mysqld ne viole pas le profil ...
trojjer
4

Une autre approche: avoir une autre instance de MySQL exécutée dans un tempfs qui utilise un disque RAM. Instructions dans ce billet de blog: Accélérer MySQL pour les tests dans Django .

Avantages:

  • Vous utilisez exactement la même base de données que celle utilisée par votre serveur de production
  • pas besoin de changer votre configuration mysql par défaut
neves
la source
2

En prolongeant la réponse d'Anurag, j'ai simplifié le processus en créant les mêmes test_settings et en ajoutant ce qui suit à 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")

semble plus propre puisque sys est déjà importé et manage.py n'est utilisé que via la ligne de commande, donc pas besoin d'encombrer les paramètres

Alvin
la source
2
Attention à cela "test" in sys.argv; il peut se déclencher lorsque vous ne le souhaitez pas, par exemple manage.py collectstatic -i test. sys.argv[1] == "test"est une condition plus précise qui ne devrait pas avoir ce problème.
retour le
2
@keturn de cette façon, il génère une exception lors de l'exécution ./manage.pysans arguments (par exemple pour voir quels plugins sont disponibles, comme --help)
Antony Hatchkins
1
@AntonyHatchkins C'est trivial à résoudre:len(sys.argv) > 1 and sys.argv[1] == "test"
DylanYoung
1
@DylanYoung Oui, c'est exactement ce que je voulais qu'Alvin ajoute à sa solution mais il n'est pas particulièrement intéressé à l'améliorer. Quoi qu'il en soit, cela ressemble plus à un piratage rapide qu'à la solution légitime.
Antony Hatchkins
1
Je n'ai pas regardé cette réponse depuis un moment, j'ai mis à jour l'extrait pour refléter l'amélioration de @ DylanYoung
Alvin
0

Utilisez ci-dessous dans votre setting.py

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