Testy jednostkowe django bez bazy danych

126

Czy istnieje możliwość pisania unittestów django bez konfigurowania bazy danych? Chcę przetestować logikę biznesową, która nie wymaga konfiguracji bazy danych. I chociaż konfiguracja bazy danych jest szybka, naprawdę nie potrzebuję jej w niektórych sytuacjach.

pawelok
źródło
Zastanawiam się, czy to rzeczywiście ma znaczenie. Baza danych jest przechowywana w pamięci, + jeśli nie masz żadnych modeli, nic nie jest wykonywane z bazą danych. Więc jeśli tego nie potrzebujesz, nie konfiguruj modeli.
Torsten Engelbrecht
3
Mam modele, ale dla tych testów nie mają one znaczenia. A baza danych nie jest przechowywana w pamięci, ale jest tworzona w mysql specjalnie w tym celu. Nie to, żebym tego chciał. Może mógłbym skonfigurować django do używania bazy danych w pamięci do testowania. Czy wiesz, jak to zrobić?
paweloque
Oh przepraszam. Bazy danych w pamięci to tylko przypadek, gdy używasz bazy danych SQLite. Poza tym nie widzę sposobu, aby uniknąć tworzenia testowej bazy danych. W dokumentacji nie ma nic na ten temat. Nigdy nie czułem potrzeby, aby tego unikać.
Torsten Engelbrecht
3
Przyjęta odpowiedź mnie nie pomogła. Zamiast tego zadziałało idealnie: caktusgroup.com/blog/2013/10/02/skipping-test-db-creation
Hugo Pineda

Odpowiedzi:

122

Możesz podklasować DjangoTestSuiteRunner i przesłonić metody setup_databases i teardown_databases do przekazania.

Utwórz nowy plik ustawień i ustaw TEST_RUNNER na nowo utworzoną klasę. Następnie podczas uruchamiania testu określ nowy plik ustawień za pomocą flagi --settings.

Oto co zrobiłem:

Utwórz niestandardowy biegacz kombinezonu testowego podobny do tego:

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

Utwórz ustawienia niestandardowe:

from mysite.settings import *

# Test runner with no database creation
TEST_RUNNER = 'mysite.scripts.testrunner.NoDbTestRunner'

Kiedy uruchamiasz swoje testy, uruchom je w następujący sposób z flagą --settings ustawioną na nowy plik ustawień:

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

AKTUALIZACJA: kwiecień / 2018

Od wersji Django 1.8 moduł został przeniesiony do .django.test.simple.DjangoTestSuiteRunner 'django.test.runner.DiscoverRunner'

Aby uzyskać więcej informacji, sprawdź oficjalną sekcję dokumentacji o niestandardowych uruchomieniach testów.

mohi666
źródło
2
Ten błąd pojawia się, gdy masz testy, które wymagają transakcji bazy danych. Oczywiście, jeśli nie masz bazy danych, nie będziesz w stanie uruchomić tych testów. Testy należy przeprowadzić oddzielnie. Jeśli po prostu uruchomisz test za pomocą python manage.py test --settings = new_settings.py, uruchomi on całą gamę innych testów z innych aplikacji, które mogą wymagać bazy danych.
mohi666
5
Zauważ, że musisz rozszerzyć SimpleTestCase zamiast TestCase dla swoich klas testowych. TestCase oczekuje bazy danych.
Ben Roberts,
9
Jeśli nie chcesz używać nowego pliku ustawień, możesz określić nowy TestRunner w wierszu poleceń za pomocą --testrunneropcji.
Bran Handley,
26
Świetna odpowiedź!! W django 1.8, z django.test.simple import DjangoTestSuiteRunner został zmieniony na z django.test.runner import DiscoverRunner Mam nadzieję, że to komuś pomoże!
Josh Brown
2
W Django 1.8 i nowszych można wprowadzić niewielką korektę powyższego kodu. Instrukcja import może zostać zmieniona na: from django.test.runner import DiscoverRunner NoDbTestRunner musi teraz rozszerzyć klasę DiscoverRunner.
Aditya Satyavada
77

Ogólnie testy w aplikacji można podzielić na dwie kategorie

  1. Testy jednostkowe testują poszczególne fragmenty kodu pod wpływem nasłonecznienia i nie wymagają przechodzenia do bazy danych
  2. Przypadki testowe integracji, które faktycznie trafiają do bazy danych i testują w pełni zintegrowaną logikę.

Django obsługuje testy jednostkowe i integracyjne.

Testy jednostkowe nie wymagają konfigurowania i niszczenia bazy danych, a te powinniśmy odziedziczyć po SimpleTestCase .

from django.test import SimpleTestCase


class ExampleUnitTest(SimpleTestCase):
    def test_something_works(self):
        self.assertTrue(True)

W przypadku przypadków testowych integracji dziedziczenie z TestCase z kolei dziedziczy z TransactionTestCase i skonfiguruje i zburzy bazę danych przed uruchomieniem każdego testu.

from django.test import TestCase


class ExampleIntegrationTest(TestCase):
    def test_something_works(self):
        #do something with database
        self.assertTrue(True)

Ta strategia zapewni, że baza danych zostanie utworzona i zniszczona tylko dla przypadków testowych, które mają dostęp do bazy danych, a zatem testy będą bardziej wydajne

Ali
źródło
37
Może to zwiększyć wydajność wykonywania testów, ale należy pamiętać, że program uruchamiający testy nadal tworzy testowe bazy danych podczas inicjalizacji.
monkut
6
O wiele prostsze, że wybrana odpowiedź. Dziękuję bardzo!
KFunk
1
@monkut Nie ... jeśli masz tylko klasę SimpleTestCase, program uruchamiający testy nic nie uruchamia, zobacz ten projekt .
Claudio Santos
Django nadal będzie próbował utworzyć testową bazę danych, nawet jeśli używasz tylko SimpleTestCase. Zobacz to pytanie .
Marko Prcać
użycie SimpleTestCase działa dokładnie w przypadku testowania metod narzędziowych lub fragmentów kodu i nie używa ani nie tworzy testowej bazy danych. Dokładnie to, czego potrzebuję!
Tyro Hunter
28

Z django.test.simple

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

Więc zastąp DiscoverRunnerzamiast 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

Użyj w ten sposób:

python manage.py test app --testrunner=app.filename.NoDbTestRunner
themadmax
źródło
8

Zdecydowałem się odziedziczyć django.test.runner.DiscoverRunneri dodać kilka elementów do run_testsmetody.

Mój pierwszy dodatek sprawdza, czy ustawienie bazy danych jest konieczne i pozwala na uruchomienie normalnej setup_databasesfunkcjonalności, jeśli baza danych jest konieczna. Mój drugi dodatek pozwala normalnie teardown_databasesdziałać, jeśli setup_databasesmetoda była dozwolona.

W moim kodzie założono, że każda sprawa TestCase, która dziedziczy z django.test.TransactionTestCase(a tym samym django.test.TestCase) wymaga skonfigurowania bazy danych. Zrobiłem to założenie, ponieważ doktorzy Django mówią:

Jeśli potrzebujesz innych, bardziej złożonych i ciężkich funkcji specyficznych dla Django, takich jak ... Testowanie lub używanie ORM ..., powinieneś zamiast tego użyć TransactionTestCase lub TestCase.

https://docs.djangoproject.com/en/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)

Na koniec dodałem następujący wiersz do pliku settings.py mojego projektu.

mysite / settings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

Teraz, gdy uruchamiam tylko testy niezależne od db, mój zestaw testów działa o rząd wielkości szybciej! :)

Paweł
źródło
6

Zaktualizowano: zapoznaj się również z tą odpowiedzią dotyczącą korzystania z narzędzia innej firmy pytest.


@Cesar ma rację. Po przypadkowym uruchomieniu ./manage.py test --settings=no_db_settings, bez określenia nazwy aplikacji, moja programistyczna baza danych została wyczyszczona.

Dla bezpieczniejszego sposobu używaj tego samego NoDbTestRunner, ale w połączeniu z następującymi 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'

Musisz utworzyć bazę danych o nazwie _test_mysite_dbprzy użyciu narzędzia zewnętrznej bazy danych. Następnie uruchom następujące polecenie, aby utworzyć odpowiednie tabele:

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

Jeśli używasz południa, uruchom również następujące polecenie:

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

DOBRZE!

Możesz teraz przeprowadzać testy jednostkowe niesamowicie szybko (i bezpiecznie) poprzez:

./manage.py test myapp --settings=mysite.no_db_settings
Rockallite
źródło
Uruchomiłem testy z użyciem pytest (z wtyczką pytest-django) i NoDbTestRunner, jeśli jakoś przypadkowo utworzysz obiekt w przypadku testowym i nie nadpisujesz nazwy bazy danych, obiekt zostanie utworzony w lokalnych bazach danych, które konfigurujesz w ustawienia. Nazwa „NoDbTestRunner” powinna brzmieć „NoTestDbTestRunner”, ponieważ nie utworzy testowej bazy danych, ale użyje Twojej bazy danych z ustawień.
Gabriel Muj
2

Jako alternatywę dla zmodyfikowania ustawień w celu uczynienia NoDbTestRunner „bezpiecznym”, oto zmodyfikowana wersja NoDbTestRunner, która zamyka bieżące połączenie z bazą danych i usuwa informacje o połączeniu z ustawień i obiektu połączenia. U mnie działa, przetestuj go w swoim środowisku, zanim na nim polegasz :)

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
Tecuya
źródło
UWAGA: Jeśli usuniesz domyślne połączenie z listy połączeń, nie będziesz mógł korzystać z modeli Django lub innych funkcji, które normalnie używają bazy danych (oczywiście nie komunikujemy się z bazą danych, ale Django sprawdza różne funkcje obsługiwane przez DB) . Wygląda też na to, że połączenia._connections już nie obsługują __getitem__. Użyj connections._connections.default, aby uzyskać dostęp do obiektu.
the_drow,
2

Innym rozwiązaniem byłoby pozostawienie klasy testowej po prostu dziedziczenia z unittest.TestCasedowolnej klasy testowej Django zamiast jej. Dokumentacja Django ( https://docs.djangoproject.com/en/2.0/topics/testing/overview/#writing-tests ) zawiera następujące ostrzeżenie:

Korzystanie z unittest.TestCase pozwala uniknąć kosztów wykonywania każdego testu w transakcji i opróżniania bazy danych, ale jeśli testy współdziałają z bazą danych, ich zachowanie będzie się różnić w zależności od kolejności ich wykonywania przez moduł uruchamiający testy. Może to prowadzić do testów jednostkowych, które przechodzą, gdy są uruchamiane w izolacji, ale kończą się niepowodzeniem, gdy są uruchamiane w zestawie.

Jeśli jednak Twój test nie korzysta z bazy danych, to ostrzeżenie nie musi Cię martwić i możesz czerpać korzyści z braku konieczności uruchamiania każdego przypadku testowego w transakcji.

Kurt Peek
źródło
Wygląda na to, że nadal tworzy i niszczy bazę danych, jedyną różnicą jest to, że nie uruchamia testu w transakcji i nie opróżnia bazy danych.
Cam Rail
0

Powyższe rozwiązania też są w porządku. Ale poniższe rozwiązanie również skróci czas tworzenia bazy danych, jeśli jest więcej migracji. Podczas testów jednostkowych uruchomienie syncdb zamiast uruchamiania wszystkich migracji na południe będzie znacznie szybsze.

SOUTH_TESTS_MIGRATE = False # Aby wyłączyć migracje i zamiast tego użyć syncdb

venkat
źródło
0

Mój host sieciowy pozwala tylko na tworzenie i usuwanie baz danych z ich internetowego interfejsu graficznego, więc podczas próby uruchomienia otrzymywałem komunikat „Wystąpił błąd podczas tworzenia testowej bazy danych: odmowa uprawnień” python manage.py test.

Miałem nadzieję, że użyję opcji --keepdb do django-admin.py, ale wydaje się, że nie jest ona już obsługiwana od wersji Django 1.7.

Skończyło się na tym, że zmodyfikowałem kod Django w ... / django / db / backends / creation.py, a konkretnie funkcje _create_test_db i _destroy_test_db.

Ponieważ _create_test_dbzakomentowałem cursor.execute("CREATE DATABASE ...linię i zastąpiłem ją, passaby tryblok nie był pusty.

Bo _destroy_test_dbwłaśnie wykomentowałem cursor.execute("DROP DATABASE- nie musiałem go niczym zastępować, ponieważ w bloku ( time.sleep(1)) było już inne polecenie .

Potem moje testy przebiegły dobrze - chociaż osobno skonfigurowałem wersję test_ mojej zwykłej bazy danych.

Nie jest to oczywiście świetne rozwiązanie, ponieważ zepsuje się, jeśli Django zostanie zaktualizowane, ale miałem lokalną kopię Django z powodu używania virtualenv, więc przynajmniej mam kontrolę nad tym, kiedy / czy zaktualizuję do nowszej wersji.

Chirael
źródło
0

Inne rozwiązanie, o którym nie wspomniano: było to dla mnie łatwe do zaimplementowania, ponieważ mam już wiele plików ustawień (dla lokalnego / tymczasowego / produkcyjnego), które dziedziczą po base.py. Więc w przeciwieństwie do innych ludzi nie musiałem nadpisywać BAZ DANYCH ['domyślne'], ponieważ BAZY DANYCH nie są ustawione w base.py

SimpleTestCase nadal próbował połączyć się z moją testową bazą danych i uruchomić migracje. Kiedy stworzyłem plik config / settings / test.py, który nie ustawiał baz danych na nic, moje testy jednostkowe działały bez niego. Pozwoliło mi to na użycie modeli, które miały klucz obcy i unikalne pola ograniczeń. (Odwrotne wyszukiwanie klucza obcego, które wymaga wyszukiwania bazy danych, kończy się niepowodzeniem).

(Django 2.0.6)

Fragmenty kodu 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
Simone
źródło
0

Używając testowego biegacza do nosa (django-nose), możesz zrobić coś takiego:

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

W swoim settings.pymożesz tam określić biegacza testowego, tj

TEST_RUNNER = 'lib.nodb_test_runner.NoDbTestRunner' . # Was 'django_nose.NoseTestSuiteRunner'

LUB

Chciałem go tylko do uruchamiania określonych testów, więc uruchamiam go tak:

python manage.py test integration_tests/integration_*  --noinput --testrunner=lib.nodb_test_runner.NoDbTestRunner
radtek
źródło