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.
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.DjangoTestSuiteRunner
ont é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.
Généralement, les tests d'une application peuvent être classés en deux catégories.
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.
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
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
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.
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! :)
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
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
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.
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
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
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
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.