test dell’unità django senza db

C’è la possibilità di scrivere django unittests senza creare un db? Voglio testare la logica aziendale che non richiede l’installazione del db. E mentre è veloce impostare un db, in realtà non ne ho bisogno in alcune situazioni.

È ansible eseguire la sottoclass di DjangoTestSuiteRunner e sovrascrivere i metodi setup_databases e teardown_databases per passare.

Crea un nuovo file di impostazioni e imposta TEST_RUNNER sulla nuova class appena creata. Quindi, quando esegui il test, specifica il nuovo file delle impostazioni con il flag –settings.

Ecco cosa ho fatto:

Crea un runner personalizzato per test suit simile a questo:

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 una configurazione 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

A partire da Django 1.8, il modulo django.test.simple.DjangoTestSuiteRunner stato spostato in 'django.test.runner.DiscoverRunner' .

Per maggiori informazioni, consultare la sezione doc ufficiale relativa ai test runner personalizzati.

Generalmente i test in un’applicazione possono essere classificati in due categorie

  1. Test unitari, testano i singoli frammenti di codice in insolazione e non richiedono l’accesso al database
  2. Casi di test di integrazione che vanno effettivamente al database e testano la logica completamente integrata.

Django supporta i test di unità e di integrazione.

Test unitari, non richiedono l’installazione e la distruzione 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 configurerà 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 garantirà che il database sia creato e distrutto solo per i casi di test che accedono al database e quindi i test saranno più efficienti

Da django.test.simple

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

Quindi sovrascrivi DiscoverRunner anziché 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 da django.test.runner.DiscoverRunner e aggiungere un paio di aggiunte al metodo run_tests .

La mia prima aggiunta controlla se la configurazione di un db è necessaria e consente la normale funzione setup_databases da avviare se è necessario un db. La mia seconda aggiunta consente di eseguire il normale setup_databases se è stato autorizzato il metodo setup_databases .

Il mio codice presuppone che qualsiasi TestCase che erediti da django.test.TransactionTestCase (e quindi django.test.TestCase ) richieda l’installazione di un database. Ho fatto questa supposizione perché i documenti di Django dicono:

Se hai bisogno di altre caratteristiche più complesse e pesanti di Django come … Test o utilizzo di ORM … allora dovresti usare TransactionTestCase o TestCase.

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

miosito / 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 classs, 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.

miosito / settings.py

 TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner' 

Ora, quando si eseguono solo test non dipendenti da db, la mia suite di test ha un ordine di grandezza più veloce! 🙂

Aggiornato: vedi anche questa risposta per l’utilizzo di uno strumento di terze parti pytest .


@ Cesare ha ragione. Dopo aver eseguito accidentalmente ./manage.py test --settings=no_db_settings , senza specificare un nome di app, il mio database di sviluppo è stato cancellato.

Per un modo più sicuro, utilizzare lo stesso NoDbTestRunner , ma in combinazione con il seguente 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 seguente comando per creare le tabelle corrispondenti:

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

Se stai utilizzando South, esegui anche il seguente comando:

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

OK!

Ora puoi eseguire test unitari incredibilmente rapidi (e sicuri) tramite:

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

In alternativa alla modifica delle impostazioni per rendere NoDbTestRunner “sicuro”, ecco una versione modificata di NoDbTestRunner che chiude la connessione al database corrente e rimuove le informazioni di connessione dalle impostazioni e dall’object di connessione. Funziona per me, testalo 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 

Un’altra soluzione sarebbe quella di avere la class di test semplicemente ereditata da unittest.TestCase invece di una qualsiasi delle classi di test di Django. I documenti Django ( https://docs.djopoproject.com/en/2.0/topics/testing/overview/#writing-tests ) contengono il seguente avviso a riguardo:

L’utilizzo di unittest.TestCase evita il costo dell’esecuzione di ciascun test in una transazione e lo svuotamento del database, ma se i test interagiscono con il database il loro comportamento varierà in base all’ordine in cui il test runner li esegue. Questo può portare a test di unità che passano quando vengono eseguiti in isolamento ma falliscono quando vengono eseguiti in una suite.

Tuttavia, se il test non utilizza il database, questo avviso non deve riguardare te e puoi trarre vantaggio dal non dover eseguire ogni test case in una transazione.

Anche le soluzioni di cui sopra vanno bene. Ma la seguente soluzione ridurrà anche il tempo di creazione del db se c’è 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 di creare e rilasciare database dalla loro GUI Web, quindi ricevo un errore “Errore di creazione del database di test: authorization negata” quando si tenta di eseguire python manage.py test .

Speravo di usare l’opzione –keepdb su django-admin.py ma non sembra più supportato da Django 1.7.

Quello che ho finito è 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 il cursor.execute("CREATE DATABASE ... line e l’ho sostituito con pass così il blocco try non sarebbe vuoto.

Per _destroy_test_db ho appena commentato cursor.execute("DROP DATABASE – Non avevo bisogno di sostituirlo con qualcosa perché c’era già un altro comando nel blocco ( time.sleep(1) ).

In seguito i miei test sono andati a buon fine, anche se ho impostato separatamente una versione di test del mio database normale.

Questa non è un’ottima soluzione, naturalmente, perché si romperà se si aggiorna Django, ma ho avuto 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: questo è stato facile da implementare perché ho già più file di impostazioni (per local / staging / production) che ereditano da base.py. Quindi, a differenza delle altre persone, non ho dovuto sovrascrivere DATABASES [‘default’], poiché DATABASES non è impostato in base.py

SimpleTestCase ha ancora provato a connettersi al mio database di test ed eseguire le migrazioni. Quando ho creato un file config / settings / test.py che non ha impostato i DATABASE su qualcosa, i miei test di unità sono stati eseguiti senza di essi. Mi ha permesso di utilizzare modelli con chiave esterna e campi di vincoli univoci. (La ricerca inversa di chiavi esterne, che richiede una ricerca db, fallisce).

(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='[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