C'è la possibilità di scrivere unittest django senza impostare un db? Voglio testare la logica di business che non richiede l'installazione del db. E mentre è veloce installare un db, in alcune situazioni non ne ho davvero bisogno.
C'è la possibilità di scrivere unittest django senza impostare un db? Voglio testare la logica di business che non richiede l'installazione del db. E mentre è veloce installare un db, in alcune situazioni non ne ho davvero bisogno.
Risposte:
È possibile sottoclassare DjangoTestSuiteRunner e sovrascrivere i metodi setup_database e teardown_database per passare.
Crea un nuovo file di impostazioni e imposta TEST_RUNNER sulla nuova classe appena creata. Quindi quando esegui il test, specifica il tuo nuovo file di impostazioni con il flag --settings.
Ecco cosa ho fatto:
Crea un runner di prova personalizzato simile al seguente:
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
Crea un'impostazione personalizzata:
from mysite.settings import *
# Test runner with no database creation
TEST_RUNNER = 'mysite.scripts.testrunner.NoDbTestRunner'
Quando esegui i test, eseguilo come segue con il flag --settings impostato sul tuo nuovo file delle impostazioni:
python manage.py test myapp --settings='no_db_settings'
AGGIORNAMENTO: aprile / 2018
Da Django 1.8, il modulo è stato spostato in .django.test.simple.DjangoTestSuiteRunner
'django.test.runner.DiscoverRunner'
Per maggiori informazioni consulta la sezione doc ufficiale sui runner di test personalizzati.
--testrunner
opzione.
Generalmente i test in un'applicazione possono essere classificati in due categorie
Django supporta sia i test unitari che quelli di integrazione.
I test unitari, non richiedono l'installazione e lo smantellamento del database e questi dovremmo ereditare da SimpleTestCase .
from django.test import SimpleTestCase
class ExampleUnitTest(SimpleTestCase):
def test_something_works(self):
self.assertTrue(True)
Per i casi di test di integrazione ereditati da TestCase a sua volta eredita da TransactionTestCase e imposterà e distruggerà il database prima di eseguire ogni test.
from django.test import TestCase
class ExampleIntegrationTest(TestCase):
def test_something_works(self):
#do something with database
self.assertTrue(True)
Questa strategia assicurerà che il database sia creato e distrutto solo per i casi di test che accedono al database e quindi i test saranno più efficienti
A partire dal django.test.simple
warnings.warn(
"The django.test.simple module and DjangoTestSuiteRunner are deprecated; "
"use django.test.runner.DiscoverRunner instead.",
RemovedInDjango18Warning)
Quindi sostituisci DiscoverRunner
invece di 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
Usa così:
python manage.py test app --testrunner=app.filename.NoDbTestRunner
Ho scelto di ereditare django.test.runner.DiscoverRunner
e fare un paio di aggiunte al run_tests
metodo.
La mia prima aggiunta verifica se è necessaria l'impostazione di un db e consente l'avvio della normale setup_databases
funzionalità se è necessario un db. La mia seconda aggiunta consente l' teardown_databases
esecuzione del normale se il setup_databases
metodo è stato eseguito.
Il mio codice presuppone che qualsiasi TestCase che eredita da django.test.TransactionTestCase
(e quindi django.test.TestCase
) richiede l'installazione di un database. Ho fatto questo presupposto perché i documenti di Django dicono:
Se hai bisogno di una delle altre funzioni specifiche di Django più complesse e pesanti come ... Test o utilizzo di ORM ... devi invece utilizzare TransactionTestCase o TestCase.
https://docs.djangoproject.com/en/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)
Infine, ho aggiunto la seguente riga al file settings.py del mio progetto.
TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'
Ora, quando eseguo solo test non dipendenti da db, la mia suite di test esegue un ordine di grandezza più velocemente! :)
Aggiornato: vedi anche questa risposta per l'utilizzo di uno strumento di terze parti pytest
.
@Cesar ha ragione. Dopo l'esecuzione accidentale ./manage.py test --settings=no_db_settings
, senza specificare il nome di un'app, il mio database di sviluppo è stato cancellato.
Per un modo più sicuro, utilizzare lo stesso NoDbTestRunner
, ma in combinazione con quanto segue 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'
È necessario creare un database chiamato _test_mysite_db
utilizzando uno strumento di database esterno. Quindi eseguire il comando seguente per creare le tabelle corrispondenti:
./manage.py syncdb --settings=mysite.no_db_settings
Se stai usando South, esegui anche il seguente comando:
./manage.py migrate --settings=mysite.no_db_settings
OK!
Ora puoi eseguire test unitari incredibilmente veloci (e sicuri):
./manage.py test myapp --settings=mysite.no_db_settings
In alternativa alla modifica delle impostazioni per rendere "sicuro" NoDbTestRunner, ecco una versione modificata di NoDbTestRunner che chiude la connessione corrente al database e rimuove le informazioni sulla connessione dalle impostazioni e dall'oggetto connessione. Funziona per me, provalo nel tuo ambiente prima di fare affidamento su di esso :)
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
__getitem__
più. Utilizzare connections._connections.default
per accedere all'oggetto.
Un'altra soluzione sarebbe quella di ereditare semplicemente la tua classe di test unittest.TestCase
anziché una qualsiasi delle classi di test di Django. I documenti Django ( https://docs.djangoproject.com/en/2.0/topics/testing/overview/#writing-tests ) contengono il seguente avviso al riguardo:
L'uso di unittest.TestCase evita il costo di eseguire ciascun test in una transazione e svuotare il database, ma se i test interagiscono con il database il loro comportamento varierà in base all'ordine in cui il runner di test li esegue. Ciò può portare a test unitari che vengono eseguiti quando eseguiti in modo isolato ma non funzionano quando vengono eseguiti in una suite.
Tuttavia, se il test non utilizza il database, questo avviso non deve interessarti e puoi trarre vantaggio dal non dover eseguire ogni test in una transazione.
Anche le soluzioni di cui sopra vanno bene. Ma la seguente soluzione ridurrà anche il tempo di creazione del database se ci sono più numero di migrazioni. Durante i test unitari, eseguire syncdb invece di eseguire tutte le migrazioni del sud sarà molto più veloce.
SOUTH_TESTS_MIGRATE = False # Per disabilitare le migrazioni e utilizzare invece syncdb
Il mio host web consente solo la creazione e l'eliminazione di database dalla loro GUI Web, quindi ho visualizzato l'errore "Errore durante la creazione del database di test: autorizzazione negata" durante il tentativo di esecuzione python manage.py test
.
Speravo di usare l'opzione --keepdb per django-admin.py ma non sembra più essere supportato da Django 1.7.
Quello che ho finito per fare è stato modificare il codice Django in ... / django / db / backends / creation.py, in particolare le funzioni _create_test_db e _destroy_test_db.
Per _create_test_db
ho commentato la cursor.execute("CREATE DATABASE ...
linea e sostituito con pass
così il try
blocco non sarebbe vuoto.
Perché _destroy_test_db
ho appena commentato cursor.execute("DROP DATABASE
- non avevo bisogno di sostituirlo con nulla perché c'era già un altro comando nel blocco ( time.sleep(1)
).
Dopo di che i miei test hanno funzionato bene, anche se ho impostato separatamente una versione test_ del mio database normale.
Naturalmente questa non è un'ottima soluzione, perché si romperà se Django viene aggiornato, ma avevo una copia locale di Django a causa dell'utilizzo di virtualenv, quindi almeno ho il controllo su quando / se eseguo l'aggiornamento a una versione più recente.
Un'altra soluzione non menzionata: questa è stata facile da implementare perché ho già più file di impostazioni (per local / staging / produzione) che ereditano da base.py. Quindi, diversamente dalle altre persone, non ho dovuto sovrascrivere DATABASES ['default'], dato che DATABASES non è impostato in base.py
SimpleTestCase ha ancora tentato di connettersi al mio database di test ed eseguire migrazioni. Quando ho creato un file config / settings / test.py che non ha impostato DATABASES su nulla, i test delle mie unità sono stati eseguiti senza di essa. Mi ha permesso di utilizzare modelli con chiave esterna e campi di vincolo univoci. (La ricerca inversa della chiave esterna, che richiede una ricerca db, non riesce.)
(Django 2.0.6)
Snippet di codice 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
Quando si utilizza il test del naso (django-nose), è possibile fare qualcosa del genere:
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
Nel tuo settings.py
puoi specificare il test runner lì, cioè
TEST_RUNNER = 'lib.nodb_test_runner.NoDbTestRunner' . # Was 'django_nose.NoseTestSuiteRunner'
O
Lo volevo solo per l'esecuzione di test specifici, quindi lo eseguo in questo modo:
python manage.py test integration_tests/integration_* --noinput --testrunner=lib.nodb_test_runner.NoDbTestRunner