Django unit без db

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

Ответ 1

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

Создайте новый файл настроек и установите 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'.

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

Ответ 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)

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

Ответ 3

От 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

Ответ 4

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

Мое первое дополнение проверяет, нужна ли настройка db и позволяет нормальной функции setup_databases запускаться, если требуется db. Мое второе добавление позволяет запустить нормальный 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-зависимых тестов, мой тестовый набор работает на порядок быстрее!:)

Ответ 5

Обновлено: также см. этот ответ для использования стороннего инструмента 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

OK!

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

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

Ответ 6

В качестве альтернативы модификации ваших настроек для обеспечения безопасности "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

Ответ 7

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

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

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

Ответ 8

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

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

Ответ 9

Мой веб-хост разрешает создавать и удалять базы данных из своего веб-графического интерфейса, поэтому при попытке запуска 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, так что, по крайней мере, у меня есть контроль над тем, когда/если я обновляюсь до более новой версии.

Ответ 10

Другое решение, не упомянутое: это было легко реализовать, потому что у меня уже есть несколько файлов настроек (для локальных /staging/production), которые наследуются от base.py. Таким образом, в отличие от других людей, мне не нужно было перезаписывать DATABASES ['default'], так как DATABASES не установлен в base.py

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

(Джанго 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='[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

Ответ 11

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

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