tests unitaires django sans base de données


126

Existe-t-il une possibilité d'écrire des unittests django sans configurer de base de données? Je veux tester la logique métier qui ne nécessite pas la configuration de la base de données. Et bien qu'il soit rapide de configurer une base de données, je n'en ai vraiment pas besoin dans certaines situations.


Je me demande si cela compte réellement. La base de données est conservée en mémoire + si vous n'avez aucun modèle, rien n'est effectué avec la base de données. Donc, si vous n'en avez pas besoin, ne configurez pas de modèles.
Torsten Engelbrecht

3
J'ai des modèles, mais pour ces tests, ils ne sont pas pertinents. Et la base de données n'est pas conservée dans la mémoire, mais construite dans mysql, spécifiquement à cette fin. Non pas que je veuille cela. Peut-être que je pourrais configurer django pour utiliser une base de données en mémoire pour les tests. Sais-tu comment faire cela?
paweloque

Oh je suis désolé. Les bases de données en mémoire ne sont que le cas lorsque vous utilisez une base de données SQLite. Sauf cela, je ne vois pas de moyen d'éviter de créer la base de données de test. Il n'y a rien à ce sujet dans la documentation + je n'ai jamais ressenti le besoin de l'éviter.
Torsten Engelbrecht

3
La réponse acceptée ne m'a pas fonctionné. Au lieu de cela, cela a parfaitement fonctionné: caktusgroup.com/blog/2013/10/02/skipping-test-db-creation
Hugo Pineda

Réponses:


122

Vous pouvez sous-classer DjangoTestSuiteRunner et remplacer les méthodes setup_databases et teardown_databases à transmettre.

Créez un nouveau fichier de paramètres et définissez TEST_RUNNER sur la nouvelle classe que vous venez de créer. Ensuite, lorsque vous exécutez votre test, spécifiez votre nouveau fichier de paramètres avec l'indicateur --settings.

Voici ce que j'ai fait:

Créez un coureur de combinaison de test personnalisé similaire à celui-ci:

from django.test.simple import DjangoTestSuiteRunner

class NoDbTestRunner(DjangoTestSuiteRunner):
  """ A test runner to test without database creation """

  def setup_databases(self, **kwargs):
    """ Override the database creation defined in parent class """
    pass

  def teardown_databases(self, old_config, **kwargs):
    """ Override the database teardown defined in parent class """
    pass

Créez des paramètres personnalisés:

from mysite.settings import *

# Test runner with no database creation
TEST_RUNNER = 'mysite.scripts.testrunner.NoDbTestRunner'

Lorsque vous exécutez vos tests, exécutez-les comme suit avec l'indicateur --settings défini sur votre nouveau fichier de paramètres:

python manage.py test myapp --settings='no_db_settings'

MISE À JOUR: avril / 2018

Depuis Django 1.8, le module a été déplacé vers .django.test.simple.DjangoTestSuiteRunner 'django.test.runner.DiscoverRunner'

Pour plus d'informations, consultez la section officielle de la documentation sur les testeurs personnalisés.


2
Cette erreur se produit lorsque vous avez des tests qui nécessitent des transactions de base de données. Évidemment, si vous n'avez pas de base de données, vous ne pourrez pas exécuter ces tests. Vous devez exécuter vos tests séparément. Si vous exécutez simplement votre test en utilisant python manage.py test --settings = new_settings.py, il va exécuter tout un tas d'autres tests à partir d'autres applications qui peuvent nécessiter une base de données.
mohi666

5
Notez que vous devrez étendre SimpleTestCase au lieu de TestCase pour vos classes de test. TestCase attend une base de données.
Ben Roberts

9
Si vous ne souhaitez pas utiliser un nouveau fichier de paramètres, vous pouvez spécifier le nouveau TestRunner sur la ligne de commande avec l' --testrunneroption.
Bran Handley

26
Très bonne réponse!! Dans django 1.8, depuis django.test.simple import DjangoTestSuiteRunner a été changé pour depuis django.test.runner import DiscoverRunner J'espère que ça aide quelqu'un!
Josh Brown

2
Dans Django 1.8 et supérieur, une légère correction du code ci-dessus peut être apportée. L'instruction import peut être modifiée comme suit: from django.test.runner import DiscoverRunner Le NoDbTestRunner doit maintenant étendre la classe DiscoverRunner.
Aditya Satyavada

77

En règle générale, les tests d'une application peuvent être classés en deux catégories

  1. Tests unitaires, ceux-ci testent les extraits de code individuels dans l'insolation et ne nécessitent pas d'aller à la base de données
  2. Cas de test d'intégration qui vont réellement à la base de données et testent la logique entièrement intégrée

Django prend en charge les tests unitaires et d'intégration.

Les tests unitaires ne nécessitent pas de configuration et de suppression de la base de données et nous devrions hériter de SimpleTestCase .

from django.test import SimpleTestCase


class ExampleUnitTest(SimpleTestCase):
    def test_something_works(self):
        self.assertTrue(True)

Pour les cas de test d'intégration, hériter de TestCase hérite à son tour de TransactionTestCase et il configurera et supprimera la base de données avant d'exécuter chaque test.

from django.test import TestCase


class ExampleIntegrationTest(TestCase):
    def test_something_works(self):
        #do something with database
        self.assertTrue(True)

Cette stratégie garantira que la base de données sera créée et détruite uniquement pour les cas de test qui accèdent à la base de données et donc les tests seront plus efficaces


37
Cela peut rendre l'exécution des tests plus efficace, mais notez que le lanceur de tests crée toujours des bases de données de test lors de l'initialisation.
monkut

6
Tellement plus simple que la réponse choisie. Merci beaucoup!
KFunk

1
@monkut Non ... si vous n'avez que la classe SimpleTestCase, le testeur ne lance rien, voir ce projet .
Claudio Santos

Django essaiera toujours de créer une base de données de test même si vous n'utilisez que SimpleTestCase. Voir cette question .
Marko Prcać

l'utilisation de SimpleTestCase fonctionne exactement pour tester des méthodes utilitaires ou des extraits de code et n'utilise ni ne crée de base de données de test. Exactement ce dont j'ai besoin!
Tyro Hunter

28

De django.test.simple

  warnings.warn(
      "The django.test.simple module and DjangoTestSuiteRunner are deprecated; "
      "use django.test.runner.DiscoverRunner instead.",
      RemovedInDjango18Warning)

Alors remplacez DiscoverRunnerau lieu de DjangoTestSuiteRunner.

 from django.test.runner import DiscoverRunner

 class NoDbTestRunner(DiscoverRunner):
   """ A test runner to test without database creation/deletion """

   def setup_databases(self, **kwargs):
     pass

   def teardown_databases(self, old_config, **kwargs):
     pass

Utilisez comme ça:

python manage.py test app --testrunner=app.filename.NoDbTestRunner

8

J'ai choisi d'hériter django.test.runner.DiscoverRunneret de faire quelques ajouts à la run_testsméthode.

Mon premier ajout vérifie si la configuration d'une base de données est nécessaire et permet à la setup_databasesfonctionnalité normale de démarrer si une base de données est nécessaire. Mon deuxième ajout permet à la normale teardown_databasesde fonctionner si lesetup_databases méthode était autorisée à s'exécuter.

Mon code suppose que tout TestCase qui hérite de django.test.TransactionTestCase(et donc django.test.TestCase) nécessite la configuration d'une base de données. J'ai fait cette hypothèse parce que la documentation Django dit:

Si vous avez besoin de l'une des autres fonctionnalités plus complexes et plus lourdes spécifiques à Django comme ... Tester ou utiliser l'ORM ... alors vous devriez utiliser TransactionTestCase ou TestCase à la place.

https://docs.djangoproject.com/en/1.6/topics/testing/tools/#django.test.SimpleTestCase

monsite / scripts / settings.py

from django.test import TransactionTestCase     
from django.test.runner import DiscoverRunner


class MyDiscoverRunner(DiscoverRunner):
    def run_tests(self, test_labels, extra_tests=None, **kwargs):
        """
        Run the unit tests for all the test labels in the provided list.

        Test labels should be dotted Python paths to test modules, test
        classes, or test methods.

        A list of 'extra' tests may also be provided; these tests
        will be added to the test suite.

        If any of the tests in the test suite inherit from
        ``django.test.TransactionTestCase``, databases will be setup. 
        Otherwise, databases will not be set up.

        Returns the number of tests that failed.
        """
        self.setup_test_environment()
        suite = self.build_suite(test_labels, extra_tests)
        # ----------------- First Addition --------------
        need_databases = any(isinstance(test_case, TransactionTestCase) 
                             for test_case in suite)
        old_config = None
        if need_databases:
        # --------------- End First Addition ------------
            old_config = self.setup_databases()
        result = self.run_suite(suite)
        # ----------------- Second Addition -------------
        if need_databases:
        # --------------- End Second Addition -----------
            self.teardown_databases(old_config)
        self.teardown_test_environment()
        return self.suite_result(suite, result)

Enfin, j'ai ajouté la ligne suivante au fichier settings.py de mon projet.

monsite / settings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

Maintenant, lorsque j'exécute uniquement des tests non dépendants de la base de données, ma suite de tests exécute un ordre de grandeur plus rapide! :)


6

Mise à jour: consultez également cette réponse pour l'utilisation d'un outil tiers pytest.


@Cesar a raison. Après avoir accidentellement couru./manage.py test --settings=no_db_settings , sans spécifier de nom d'application, ma base de données de développement a été effacée.

Pour une manière plus sûre, utilisez le même NoDbTestRunner, mais en conjonction avec ce qui suit mysite/no_db_settings.py:

from mysite.settings import *

# Test runner with no database creation
TEST_RUNNER = 'mysite.scripts.testrunner.NoDbTestRunner'

# Use an alternative database as a safeguard against accidents
DATABASES['default']['NAME'] = '_test_mysite_db'

Vous devez créer une base de données appelée à l' _test_mysite_dbaide d'un outil de base de données externe. Exécutez ensuite la commande suivante pour créer les tables correspondantes:

./manage.py syncdb --settings=mysite.no_db_settings

Si vous utilisez South, exécutez également la commande suivante:

./manage.py migrate --settings=mysite.no_db_settings

D'ACCORD!

Vous pouvez désormais exécuter des tests unitaires extrêmement rapidement (et en toute sécurité) en:

./manage.py test myapp --settings=mysite.no_db_settings

J'ai exécuté des tests en utilisant pytest (avec le plugin pytest-django) et NoDbTestRunner, si d'une manière ou d'une autre vous créez un objet par accident dans un testcase et que vous ne remplacez pas le nom de la base de données, l'objet sera créé dans vos bases de données locales que vous configurez dans le paramètres. Le nom «NoDbTestRunner» doit être «NoTestDbTestRunner» car il ne créera pas la base de données de test, mais utilisera votre base de données à partir des paramètres.
Gabriel Muj

2

Au lieu de modifier vos paramètres pour rendre NoDbTestRunner "sûr", voici une version modifiée de NoDbTestRunner qui ferme la connexion à la base de données actuelle et supprime les informations de connexion des paramètres et de l'objet de connexion. Fonctionne pour moi, testez-le dans votre environnement avant de vous y fier :)

class NoDbTestRunner(DjangoTestSuiteRunner):
    """ A test runner to test without database creation """

    def __init__(self, *args, **kwargs):
        # hide/disconnect databases to prevent tests that 
        # *do* require a database which accidentally get 
        # run from altering your data
        from django.db import connections
        from django.conf import settings
        connections.databases = settings.DATABASES = {}
        connections._connections['default'].close()
        del connections._connections['default']
        super(NoDbTestRunner,self).__init__(*args,**kwargs)

    def setup_databases(self, **kwargs):
        """ Override the database creation defined in parent class """
        pass

    def teardown_databases(self, old_config, **kwargs):
        """ Override the database teardown defined in parent class """
        pass

REMARQUE: Si vous supprimez la connexion par défaut de la liste des connexions, vous ne pourrez pas utiliser les modèles Django ou d'autres fonctionnalités qui utilisent normalement la base de données (évidemment, nous ne communiquons pas avec la base de données mais Django vérifie différentes fonctionnalités que la base de données prend en charge) . Il semble également que connections._connections ne supporte __getitem__plus. Utilisez connections._connections.defaultpour accéder à l'objet.
the_drow

2

Une autre solution serait que votre classe de test hérite simplement de la unittest.TestCaseplace de l'une des classes de test de Django. La documentation Django ( https://docs.djangoproject.com/en/2.0/topics/testing/overview/#writing-tests ) contient l'avertissement suivant à ce sujet:

L'utilisation d'unittest.TestCase évite le coût de l'exécution de chaque test dans une transaction et du vidage de la base de données, mais si vos tests interagissent avec la base de données, leur comportement variera en fonction de l'ordre dans lequel le testeur les exécute. Cela peut conduire à des tests unitaires qui réussissent lorsqu'ils sont exécutés de manière isolée, mais échouent lorsqu'ils sont exécutés dans une suite.

Cependant, si votre test n'utilise pas la base de données, cet avertissement ne doit pas vous concerner et vous pouvez profiter des avantages de ne pas avoir à exécuter chaque scénario de test dans une transaction.


Il semble que cela crée et détruit toujours la base de données, la seule différence est qu'elle n'exécute pas le test dans une transaction et ne vide pas la base de données.
Cam Rail

0

Les solutions ci-dessus conviennent également. Mais la solution suivante réduira également le temps de création de la base de données s'il y a plus de migrations. Pendant les tests unitaires, exécuter syncdb au lieu d'exécuter toutes les migrations sud sera beaucoup plus rapide.

SOUTH_TESTS_MIGRATE = False # Pour désactiver les migrations et utiliser syncdb à la place


0

Mon hôte Web autorise uniquement la création et la suppression de bases de données à partir de leur interface graphique Web, donc j'obtenais une erreur "Une erreur lors de la création de la base de données de test: autorisation refusée" lors de la tentative d'exécution python manage.py test.

J'espérais utiliser l'option --keepdb pour django-admin.py mais elle ne semble plus être prise en charge depuis Django 1.7.

Ce que j'ai fini par faire, c'est de modifier le code Django dans ... / django / db / backends / creation.py, en particulier les fonctions _create_test_db et _destroy_test_db.

Car _create_test_dbj'ai commenté la cursor.execute("CREATE DATABASE ...ligne et l' ai remplacée par passpour que le trybloc ne soit pas vide.

Car _destroy_test_dbje viens de commenter cursor.execute("DROP DATABASE- je n'ai pas eu besoin de le remplacer par quoi que ce soit car il y avait déjà une autre commande dans le bloc ( time.sleep(1)).

Après cela, mes tests se sont bien déroulés - même si j'ai configuré séparément une version test_ de ma base de données régulière.

Ce n'est pas une excellente solution bien sûr, car elle se cassera si Django est mis à niveau, mais j'avais une copie locale de Django en raison de l'utilisation de virtualenv, donc au moins j'ai le contrôle sur quand / si je mets à niveau vers une version plus récente.


0

Autre solution non mentionnée: cela a été facile pour moi à mettre en œuvre car j'ai déjà plusieurs fichiers de paramètres (pour local / staging / production) qui héritent de base.py. Donc, contrairement à d'autres personnes, je n'ai pas eu à remplacer DATABASES ['default'], car DATABASES n'est pas défini dans base.py

SimpleTestCase a toujours essayé de se connecter à ma base de données de test et d'exécuter des migrations. Lorsque j'ai créé un fichier config / settings / test.py qui ne définissait DATABASES sur rien, mes tests unitaires se sont déroulés sans. Cela m'a permis d'utiliser des modèles qui avaient une clé étrangère et des champs de contrainte uniques. (La recherche inversée de clé étrangère, qui nécessite une recherche de base de données, échoue.)

(Django 2.0.6)

Extraits de code PS

PROJECT_ROOT_DIR/config/settings/test.py:
from .base import *
#other test settings

#DATABASES = {
# 'default': {
#   'ENGINE': 'django.db.backends.sqlite3',
#   'NAME': 'PROJECT_ROOT_DIR/db.sqlite3',
# }
#}

cli, run from PROJECT_ROOT_DIR:
./manage.py test path.to.app.test --settings config.settings.test

path/to/app/test.py:
from django.test import SimpleTestCase
from .models import *
#^assume models.py imports User and defines Classified and UpgradePrice

class TestCaseWorkingTest(SimpleTestCase):
  def test_case_working(self):
    self.assertTrue(True)
  def test_models_ok(self):
    obj = UpgradePrice(title='test',price=1.00)
    self.assertEqual(obj.title,'test')
  def test_more_complex_model(self):
    user = User(username='testuser',email='hi@hey.com')
    self.assertEqual(user.username,'testuser')
  def test_foreign_key(self):
    user = User(username='testuser',email='hi@hey.com')
    ad = Classified(user=user,headline='headline',body='body')
    self.assertEqual(ad.user.username,'testuser')
  #fails with error:
  def test_reverse_foreign_key(self):
    user = User(username='testuser',email='hi@hey.com')
    ad = Classified(user=user,headline='headline',body='body')
    print(user.classified_set.first())
    self.assertTrue(True) #throws exception and never gets here

0

Lorsque vous utilisez le testeur de nez (django-nose), vous pouvez faire quelque chose comme ceci:

my_project/lib/nodb_test_runner.py:

from django_nose import NoseTestSuiteRunner


class NoDbTestRunner(NoseTestSuiteRunner):
    """
    A test runner to test without database creation/deletion
    Used for integration tests
    """
    def setup_databases(self, **kwargs):
        pass

    def teardown_databases(self, old_config, **kwargs):
        pass

Dans votre, settings.pyvous pouvez spécifier le testeur ici, c'est-à-dire

TEST_RUNNER = 'lib.nodb_test_runner.NoDbTestRunner' . # Was 'django_nose.NoseTestSuiteRunner'

OU

Je le voulais uniquement pour exécuter des tests spécifiques, alors je le lance comme suit:

python manage.py test integration_tests/integration_*  --noinput --testrunner=lib.nodb_test_runner.NoDbTestRunner
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.