модульные тесты Django без БД - PullRequest
104 голосов
/ 07 мая 2011

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

Ответы [ 11 ]

107 голосов
/ 10 августа 2011

Вы можете создать подкласс 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'.

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

55 голосов
/ 09 января 2014

Как правило, тесты в приложении можно разделить на две категории

  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)

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

27 голосов
/ 11 июля 2014

С 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
8 голосов
/ 16 октября 2014

Я выбрал наследование от 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 / 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)

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

mysite / settings.py

TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'

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

6 голосов
/ 18 июня 2013

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

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

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

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

2 голосов
/ 16 августа 2013

В качестве альтернативы изменению ваших настроек, чтобы сделать 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
0 голосов
/ 06 февраля 2019

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

my_project/lib/nodb_test_runner.py * * 1004

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'

OR

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

python manage.py test integration_tests/integration_*  --noinput --testrunner=lib.nodb_test_runner.NoDbTestRunner
0 голосов
/ 11 июля 2018

Другое решение, не упомянутое: это было легко реализовать, потому что у меня уже есть несколько файлов настроек (для локальных / промежуточных / производственных), которые наследуются от base.py.Поэтому, в отличие от других людей, мне не пришлось перезаписывать DATABASES ['default'], так как DATABASES не задан в 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 голосов
/ 04 января 2015

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

I я получаю сообщение об ошибке «Произошла ошибка при создании тестовой базы данных: отказано в доступе».Я надеялся использовать параметр --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)).

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...