django unit test senza un db


126

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.


Mi chiedo se questo sia davvero importante. Il db viene tenuto in memoria + se non si dispone di alcun modello, non viene eseguito nulla con il db. Quindi se non ne hai bisogno non impostare modelli.
Torsten Engelbrecht,

3
Ho dei modelli, ma per quei test non sono rilevanti. E il db non è tenuto in memoria, ma costruito in mysql, tuttavia, appositamente per questo scopo. Non che lo voglia .. Forse potrei configurare django per usare un db in memoria per i test. Sai come si fa?
paweloque,

Oh mi dispiace. I database in memoria sono il caso giusto quando si utilizza un database SQLite. Tranne questo, non vedo un modo per evitare di creare il test db. Non c'è nulla al riguardo nei documenti + Non ho mai sentito il bisogno di evitarlo.
Torsten Engelbrecht,

3
La risposta accettata non mi ha funzionato. Invece, ha funzionato perfettamente: caktusgroup.com/blog/2013/10/02/skipping-test-db-creation
Hugo Pineda

Risposte:


122

È 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.


2
Questo errore viene generato quando si hanno test che richiedono transazioni di database. Ovviamente se non hai un DB, non sarai in grado di eseguire quei test. È necessario eseguire i test separatamente. Se esegui semplicemente il tuo test usando python manage.py test --settings = new_settings.py, eseguirà un sacco di altri test da altre app che potrebbero richiedere database.
mohi666,

5
Nota che dovrai estendere SimpleTestCase invece di TestCase per le tue classi di test. TestCase prevede un database.
Ben Roberts,

9
Se non si desidera utilizzare un nuovo file di impostazioni, è possibile specificare il nuovo TestRunner sulla riga di comando con l' --testrunneropzione.
Bran Handley,

26
Bella risposta!! In django 1.8, da django.test.simple import DjangoTestSuiteRunner è stato cambiato da django.test.runner import ScopriRunner Spero che aiuti qualcuno!
Josh Brown,

2
In Django 1.8 e versioni successive, è possibile apportare una leggera correzione al codice sopra. L'istruzione import può essere modificata in: da django.test.runner import DiscoverRunner Adesso NoDbTestRunner deve estendere la classe DiscoverRunner.
Aditya Satyavada,

77

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

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

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


37
Ciò potrebbe rendere più efficienti l'esecuzione dei test, ma si noti che il runner di test crea ancora database di test all'inizializzazione.
monkut

6
Molto più semplice della risposta scelta. Grazie mille!
KFunk,

1
@monkut No ... se hai solo la classe SimpleTestCase, il test runner non esegue nulla, vedi questo progetto .
Claudio Santos,

Django proverà comunque a creare un DB di prova anche se si utilizza solo SimpleTestCase. Vedere questa domanda .
Marko Prcać,

l'utilizzo di SimpleTestCase funziona esattamente per testare metodi di utilità o snippet e non utilizza né crea db di test. Esattamente quello che mi serve!
Tyro Hunter,

28

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 DiscoverRunnerinvece 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

8

Ho scelto di ereditare django.test.runner.DiscoverRunnere fare un paio di aggiunte al run_testsmetodo.

La mia prima aggiunta verifica se è necessaria l'impostazione di un db e consente l'avvio della normale setup_databasesfunzionalità se è necessario un db. La mia seconda aggiunta consente l' teardown_databasesesecuzione del normale se il setup_databasesmetodo è 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

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

miosito / settings.py

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! :)


6

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_dbutilizzando 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

Ho eseguito dei test usando pytest (con plug-in pytest-django) e NoDbTestRunner, se in qualche modo crei un oggetto per errore in una testcase e non sovrascrivi il nome del database, l'oggetto verrà creato nei tuoi database locali che hai impostato nel impostazioni. Il nome "NoDbTestRunner" dovrebbe essere "NoTestDbTestRunner" perché non creerà il database di prova, ma utilizzerà il database dalle impostazioni.
Gabriel Muj,

2

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

NOTA: se si elimina la connessione predefinita dall'elenco delle connessioni non sarà possibile utilizzare i modelli Django o altre funzionalità che normalmente utilizzano il database (ovviamente non comunichiamo con il database ma Django controlla diverse funzionalità supportate dal DB) . Inoltre sembra che connections._connections non supporti __getitem__più. Utilizzare connections._connections.defaultper accedere all'oggetto.
the_drow

2

Un'altra soluzione sarebbe quella di ereditare semplicemente la tua classe di test unittest.TestCaseanziché 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.


Sembra che questo crei e distrugga ancora il db, l'unica differenza è che non esegue il test in una transazione e non scarica il db.
Cam Rail

0

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


0

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_dbho commentato la cursor.execute("CREATE DATABASE ...linea e sostituito con passcosì il tryblocco non sarebbe vuoto.

Perché _destroy_test_dbho 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.


0

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

0

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.pypuoi 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.