Модульные тесты django без db

126

Есть ли возможность писать юнит-тесты django без настройки db? Я хочу протестировать бизнес-логику, которая не требует настройки базы данных. И хотя БД настраивается быстро, мне это действительно не нужно в некоторых ситуациях.

paweloque
источник
Мне интересно, действительно ли это имеет значение. БД хранится в памяти + если у вас нет моделей, с БД ничего не происходит. Так что, если вам это не нужно, не настраивайте модели.
Торстен Энгельбрехт
3
У меня есть модели, но для тех тестов они не актуальны. И db не хранится в памяти, а создается в mysql, однако, специально для этой цели. Не то чтобы я этого хотел ... Возможно, я мог бы настроить django на использование базы данных в памяти для тестирования. Ты знаешь как это сделать?
paweloque
Ах, прошу прощения. Базы данных в памяти - это тот случай, когда вы используете базу данных SQLite. Кроме этого, я не вижу способа избежать создания тестовой базы данных. В документации об этом нет ничего + Я никогда не чувствовал необходимости избегать этого.
Торстен Энгельбрехт
3
Принятый ответ меня не устроил. Вместо этого это сработало отлично: caktusgroup.com/blog/2013/10/02/skipping-test-db-creation
Хьюго Пинеда,

Ответы:

122

Вы можете создать подкласс DjangoTestSuiteRunner и переопределить методы setup_databases и teardown_databases для передачи.

Создайте новый файл настроек и установите TEST_RUNNER на новый класс, который вы только что создали. Затем, когда вы запускаете свой тест, укажите новый файл настроек с флагом --settings.

Вот что я сделал:

Создайте собственный бегун тестового костюма, подобный этому:

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

Создайте собственные настройки:

from mysite.settings import *

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

Когда вы запускаете свои тесты, запустите его, как показано ниже, с флагом --settings, установленным для вашего нового файла настроек:

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

ОБНОВЛЕНИЕ: апрель / 2018

Начиная с Django 1.8, модуль был перемещен в .django.test.simple.DjangoTestSuiteRunner 'django.test.runner.DiscoverRunner'

Для получения дополнительной информации ознакомьтесь с разделом официальной документации о пользовательских средствах запуска тестов.

mohi666
источник
2
Эта ошибка возникает, когда у вас есть тесты, требующие транзакций с базой данных. Очевидно, что если у вас нет БД, вы не сможете запускать эти тесты. Вы должны запускать свои тесты отдельно. Если вы просто запустите свой тест с помощью python manage.py test --settings = new_settings.py, он будет запускать целый ряд других тестов из других приложений, которым может потребоваться база данных.
mohi666
5
Обратите внимание, что вам нужно расширить SimpleTestCase вместо TestCase для ваших тестовых классов. TestCase ожидает базу данных.
Бен Робертс
9
Если вы не хотите использовать новый файл настроек, вы можете указать новый TestRunner в командной строке с помощью --testrunnerпараметра.
Бран Хэндли
26
Отличный ответ !! В django 1.8 импорт из django.test.simple DjangoTestSuiteRunner был изменен на импорт из django.test.runner DiscoverRunner Надеюсь, это поможет кому-то!
Джош Браун
2
В Django 1.8 и выше можно внести небольшую поправку в приведенный выше код. Оператор импорта может быть изменен на: from django.test.runner import DiscoverRunner NoDbTestRunner теперь должен расширять класс DiscoverRunner.
Aditya Satyavada
77

Обычно тесты в приложении можно разделить на две категории.

  1. Модульные тесты, они проверяют отдельные фрагменты кода в инсоляции и не требуют обращения к базе данных.
  2. Тестовые примеры интеграции, которые фактически переходят в базу данных и тестируют полностью интегрированную логику.

Django поддерживает как модульные, так и интеграционные тесты.

Модульные тесты не требуют установки и удаления базы данных, и мы должны унаследовать их от SimpleTestCase .

from django.test import SimpleTestCase


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

Для интеграционных тестовых случаев наследование от TestCase, в свою очередь, наследуется от TransactionTestCase, и он будет настраивать и разрушать базу данных перед запуском каждого теста.

from django.test import TestCase


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

Эта стратегия гарантирует, что база данных будет создана и уничтожена только для тестовых случаев, которые обращаются к базе данных, и поэтому тесты будут более эффективными.

Али
источник
37
Это может сделать выполнение тестов более эффективным, но обратите внимание, что средство запуска тестов по-прежнему создает тестовые базы данных при инициализации.
монкут
6
Настолько проще, что выбранный ответ. Спасибо вам большое!
KFunk
1
@monkut Нет ... если у вас есть только класс SimpleTestCase, средство запуска тестов ничего не запускает, см. этот проект .
Клаудио Сантос
Django все равно будет пытаться создать тестовую БД, даже если вы используете только SimpleTestCase. См. Этот вопрос .
Марко Пркач
использование SimpleTestCase точно работает для тестирования служебных методов или фрагментов и не использует и не создает тестовую базу данных. Именно то, что мне нужно!
Tyro Hunter
28

Из django.test.simple

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

Так что переопределите DiscoverRunnerвместо 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

Используйте так:

python manage.py test app --testrunner=app.filename.NoDbTestRunner
themadmax
источник
8

Я решил унаследовать django.test.runner.DiscoverRunnerэтот run_testsметод и внести в него несколько дополнений .

Мое первое добавление проверяет необходимость настройки базы данных и позволяет задействовать нормальную setup_databasesфункциональность, если требуется база данных. Мое второе добавление позволяет нормальному teardown_databasesзапускать, если setup_databasesметод был разрешен.

В моем коде предполагается, что любой TestCase, который наследуется от django.test.TransactionTestCase(и, следовательно, django.test.TestCase) требует настройки базы данных. Я сделал это предположение, потому что документы Django говорят:

Если вам нужны какие-либо другие более сложные и тяжелые функции, специфичные для Django, такие как ... Тестирование или использование ORM ... тогда вам следует использовать TransactionTestCase или TestCase.

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

MySite / скрипты / 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)

Наконец, я добавил следующую строку в файл settings.py моего проекта.

MySite / settings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

Теперь, когда выполняются только тесты, не зависящие от db, мой набор тестов работает на порядок быстрее! :)

Павел
источник
6

Обновлено: также см. Этот ответ для использования стороннего инструмента pytest.


@ Сезар прав. После случайного запуска./manage.py test --settings=no_db_settings без указания имени приложения моя база данных разработки была уничтожена.

Для более безопасного использования используйте то же самое NoDbTestRunner, но в сочетании со следующим 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'

Вам необходимо создать базу данных, вызываемую _test_mysite_dbс помощью инструмента внешней базы данных. Затем выполните следующую команду, чтобы создать соответствующие таблицы:

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

Если вы используете Юг, также выполните следующую команду:

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

ХОРОШО!

Теперь вы можете запускать модульные тесты невероятно быстро (и безопасно):

./manage.py test myapp --settings=mysite.no_db_settings
Rockallite
источник
Я запускал тесты с использованием pytest (с плагином pytest-django) и NoDbTestRunner, если каким-то образом вы случайно создадите объект в тестовом примере и не переопределите имя базы данных, объект будет создан в ваших локальных базах данных, которые вы настроили в настройки. Имя «NoDbTestRunner» должно быть «NoTestDbTestRunner», потому что он не будет создавать тестовую базу данных, но будет использовать вашу базу данных из настроек.
Габриэль Мудж
2

В качестве альтернативы изменению ваших настроек, чтобы сделать NoDbTestRunner «безопасным», вот модифицированная версия NoDbTestRunner, которая закрывает текущее соединение с базой данных и удаляет информацию о соединении из настроек и объекта соединения. Работает для меня, протестируйте его в своей среде, прежде чем полагаться на него :)

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
источник
ПРИМЕЧАНИЕ. Если вы удалите соединение по умолчанию из списка подключений, вы не сможете использовать модели Django или другие функции, которые обычно используют базу данных (очевидно, что мы не взаимодействуем с базой данных, но Django проверяет различные функции, которые поддерживает БД) , Также кажется, что connections._connections больше не поддерживает __getitem__. Используйте connections._connections.defaultдля доступа к объекту.
the_drow
2

Другое решение - просто наследовать тестовый класс от unittest.TestCaseлюбого из тестовых классов Django. Документы Django ( https://docs.djangoproject.com/en/2.0/topics/testing/overview/#writing-tests ) содержат следующее предупреждение об этом:

Использование unittest.TestCase позволяет избежать затрат на запуск каждого теста в транзакции и очистку базы данных, но если ваши тесты взаимодействуют с базой данных, их поведение будет зависеть от порядка, в котором их выполняет исполнитель тестов. Это может привести к тому, что модульные тесты пройдут изолированно, но не пройдут в комплекте.

Однако, если ваш тест не использует базу данных, это предупреждение не должно вас беспокоить, и вы можете воспользоваться преимуществами отсутствия необходимости запускать каждый тестовый пример в транзакции.

Курт Пик
источник
Похоже, что это все еще создает и уничтожает базу данных, с той лишь разницей, что она не запускает тест в транзакции и не сбрасывает базу данных.
Cam Rail
0

Вышеупомянутые решения тоже подходят. Но следующее решение также сократит время создания базы данных, если будет большее количество миграций. Во время модульного тестирования запуск syncdb вместо запуска всех южных миграций будет намного быстрее.

SOUTH_TESTS_MIGRATE = False # Чтобы отключить миграции и вместо этого использовать syncdb

Venkat
источник
0

Мой веб-хостинг позволяет создавать и удалять базы данных только из своего веб-интерфейса, поэтому при попытке запуска я получал сообщение об ошибке «Получена ошибка при создании тестовой базы данных: в разрешении отказано». python manage.py test .

Я надеялся использовать параметр --keepdb для django-admin.py, но, похоже, он больше не поддерживается с Django 1.7.

В итоге я изменил код Django в ... / django / db / backends / creation.py, в частности, функции _create_test_db и _destroy_test_db.

Потому что _create_test_dbя закомментировал cursor.execute("CREATE DATABASE ...строку и заменил ее passтакtry блок не был пустым.

Потому что _destroy_test_dbя просто закомментировал cursor.execute("DROP DATABASE- мне не нужно было ничего заменять, потому что в блоке уже была другая команда (time.sleep(1) ) .

После этого мои тесты прошли нормально, хотя я настроил тестовую версию моей обычной базы данных отдельно.

Это, конечно, не лучшее решение, потому что оно сломается при обновлении Django, но у меня была локальная копия Django из-за использования virtualenv, поэтому, по крайней мере, я могу контролировать, когда / если я обновлюсь до более новой версии.

Chirael
источник
0

Другое решение, о котором не упоминалось: это было легко реализовать, потому что у меня уже есть несколько файлов настроек (для локальных / промежуточных / производственных), которые наследуются от base.py. Поэтому, в отличие от других людей, мне не пришлось перезаписывать БАЗЫ ДАННЫХ ['default'], поскольку БАЗЫ ДАННЫХ не установлены в base.py

SimpleTestCase все еще пытался подключиться к моей тестовой базе данных и выполнить миграции. Когда я создал файл config / settings / test.py, который ни для чего не устанавливал DATABASES, мои модульные тесты работали без него. Это позволило мне использовать модели с полями внешнего ключа и уникальных ограничений. (Обратный поиск внешнего ключа, который требует поиска в БД, не выполняется.)

(Django 2.0.6)

Фрагменты кода 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

При использовании средства запуска теста носа (django-носа) вы можете сделать что-то вроде этого:

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

В вашем settings.pyвы можете указать там тестового раннера, т.е.

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

ИЛИ

Я хотел, чтобы он запускал только определенные тесты, поэтому я запускаю его так:

python manage.py test integration_tests/integration_*  --noinput --testrunner=lib.nodb_test_runner.NoDbTestRunner
radtek
источник