web-dev-qa-db-fra.com

tests unitaires django sans db

Est-il possible d'écrire ununest Django sans créer une base de données? Je veux tester la logique métier qui n'exige pas que la base de données soit configurée. Et bien qu’il soit rapide de configurer une base de données, je n’en ai vraiment pas besoin dans certaines situations.

96
paweloque

Vous pouvez sous-classer DjangoTestSuiteRunner et substituer 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 programme de combinaison de test 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-le comme suit avec l'indicateur --settings défini dans votre nouveau fichier de paramètres:

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

MISE À JOUR: Avril/2018

Depuis Django 1.8, les modules Django.test.simple.DjangoTestSuiteRunneront été déplacés vers 'Django.test.runner.DiscoverRunner'.

Pour plus d'informations, consultez la section documentation officielle concernant les programmes de test personnalisés.

102
mohi666

Généralement, les tests d'une application peuvent être classés en deux catégories. 

  1. Les tests unitaires, ils testent les extraits de code individuels dans l'insolation et ne nécessitent pas d'aller à la base de données 
  2. Des cas de test d'intégration qui vont effectivement à la base de données et testent la logique entièrement intégrée.

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

Les tests unitaires, ne nécessitent pas d’installer et de démonter 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 ce dernier installera et démontera 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 est créée et détruite uniquement pour les cas de test qui accèdent à la base de données. Par conséquent, les tests seront plus efficaces.

53
Ali

De Django.test.simple

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

Donc, remplacez DiscoverRunner au 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
26
themadmax

J'ai choisi d'hériter de Django.test.runner.DiscoverRunner et d'apporter quelques ajouts à la méthode run_tests

Mon premier ajout vérifie si la configuration d'une base de données est nécessaire et permet à la fonctionnalité setup_databases normale de se déclencher si une base de données est nécessaire. Mon deuxième ajout permet au teardown_databases normal de s'exécuter si la méthode setup_databases é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 l'installation d'une base de données. J'ai émis cette hypothèse car les documents Django disent: 

Si vous avez besoin de l'une des autres fonctionnalités plus complexes et plus lourdes spécifiques à Django, telles que ... Tester ou utiliser l'ORM ..., utilisez plutôt TransactionTestCase ou TestCase.

https://docs.djangoproject.com/fr/1.6/topics/testing/tools/#Django.test.SimpleTestCase

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

mysite/settings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

Désormais, lorsque je n'exécute que des tests non dépendants de la base de données, ma suite de tests exécute un ordre de grandeur plus rapide! :)

8
Paul

Mise à jour: Voir aussi cette réponse pour utiliser un outil tiers pytest


@ César a raison. Après avoir exécuté ./manage.py test --settings=no_db_settings par inadvertance, sans spécifier de nom d'application, ma base de développement a été effacée.

Pour plus de sécurité, utilisez la même NoDbTestRunner, mais en conjonction avec le mysite/no_db_settings.py suivant:

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 _test_mysite_db à l'aide d'un outil de base de données externe. Puis exécutez 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 maintenant exécuter des tests unitaires extrêmement rapidement (et en toute sécurité) en:

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

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 en servir :)

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
2
Tecuya

Une autre solution serait que votre classe de test hérite simplement de unittest.TestCase au lieu de l’une des classes de test de Django. Les documents Django ( https://docs.djangoproject.com/fr/2.0/topics/testing/overview/#writing-tests ) contiennent l'avertissement suivant à ce sujet:

L'utilisation de unittest.TestCase évite les coûts liés à l'exécution de chaque test dans une transaction et au 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 d'exécution par le testeur. Cela peut entraîner des tests unitaires qui réussissent lorsqu'ils sont exécutés isolément mais échouent lorsqu'ils sont exécutés dans une suite.

Toutefois, si votre test n'utilise pas la base de données, cet avertissement ne vous concerne pas et vous pouvez profiter de l'avantage de ne pas avoir à exécuter chaque scénario de test dans une transaction.

2
Kurt Peek

Lorsque vous utilisez le test 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.py, vous pouvez spécifier le programme d’essai, c.-à-d.

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
0
radtek

Une autre solution non mentionnée: cela m’a été facile à mettre en œuvre car j’ai déjà plusieurs fichiers de paramètres (pour le stockage local/intermédiaire/production) qui héritent de base.py. Donc, contrairement à d'autres personnes, je n'ai pas eu à écraser BASE DE DONNÉES ['default'], car BASES DE DONNÉES n'est pas défini dans base.py

SimpleTestCase a quand même essayé de se connecter à ma base de données de test et d’effectuer des migrations. Lorsque j'ai créé un fichier config/settings/test.py qui ne définissait aucune base de données, mes tests unitaires ont été exécutés sans ce fichier. Cela m'a permis d'utiliser des modèles comportant une clé étrangère et des champs de contraintes uniques. (La recherche de clé étrangère inversée, 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='[email protected]')
    self.assertEqual(user.username,'testuser')
  def test_foreign_key(self):
    user = User(username='testuser',email='[email protected]')
    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='[email protected]')
    ad = Classified(user=user,headline='headline',body='body')
    print(user.classified_set.first())
    self.assertTrue(True) #throws exception and never gets here
0
Simone

Les solutions ci-dessus sont bien aussi. 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 . Lors des tests d'unités, exécuter syncdb au lieu de toutes les migrations vers le sud sera beaucoup plus rapide. 

SOUTH_TESTS_MIGRATE = False # Pour désactiver les migrations et utiliser syncdb au lieu

0
venkat

Mon hébergeur n'autorisant que la création et la suppression de bases de données à partir de son interface graphique Web, le message d'erreur «Vous avez une erreur lors de la création de la base de test: autorisation refusée» lorsque vous essayez d'exécuter python manage.py test.

J'avais espéré utiliser l'option --keepdb pour Django-admin.py mais cela ne semble plus être supporté depuis Django 1.7.

J'ai fini par modifier le code Django dans .../Django/db/backends/creation.py, en particulier les fonctions _create_test_db et _destroy_test_db.

Pour _create_test_db, j'ai commenté la ligne cursor.execute("CREATE DATABASE ... et l'ai remplacée par pass afin que le bloc try ne soit pas vide.

Pour _destroy_test_db je 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 - bien que j’ai configuré une version test_ de ma base de données régulière séparément.

Bien sûr, ce n’est pas une bonne solution, car cela casserait si Django était mis à niveau, mais j’avais une copie locale de Django en raison de l’utilisation de virtualenv. Au moins, je contrôle donc le moment de la mise à niveau vers une version plus récente.

0
Chirael