Есть ли возможность писать юнит-тесты django без настройки db? Я хочу протестировать бизнес-логику, которая не требует настройки базы данных. И хотя БД настраивается быстро, мне это действительно не нужно в некоторых ситуациях.
Есть ли возможность писать юнит-тесты django без настройки db? Я хочу протестировать бизнес-логику, которая не требует настройки базы данных. И хотя БД настраивается быстро, мне это действительно не нужно в некоторых ситуациях.
Ответы:
Вы можете создать подкласс 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'
Для получения дополнительной информации ознакомьтесь с разделом официальной документации о пользовательских средствах запуска тестов.
--testrunner
параметра.
Обычно тесты в приложении можно разделить на две категории.
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)
Эта стратегия гарантирует, что база данных будет создана и уничтожена только для тестовых случаев, которые обращаются к базе данных, и поэтому тесты будут более эффективными.
Из 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
Я решил унаследовать 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
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 моего проекта.
TEST_RUNNER = 'mysite.scripts.settings.MyDiscoverRunner'
Теперь, когда выполняются только тесты, не зависящие от db, мой набор тестов работает на порядок быстрее! :)
Обновлено: также см. Этот ответ для использования стороннего инструмента 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
В качестве альтернативы изменению ваших настроек, чтобы сделать 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
__getitem__
. Используйте connections._connections.default
для доступа к объекту.
Другое решение - просто наследовать тестовый класс от unittest.TestCase
любого из тестовых классов Django. Документы Django ( https://docs.djangoproject.com/en/2.0/topics/testing/overview/#writing-tests ) содержат следующее предупреждение об этом:
Использование unittest.TestCase позволяет избежать затрат на запуск каждого теста в транзакции и очистку базы данных, но если ваши тесты взаимодействуют с базой данных, их поведение будет зависеть от порядка, в котором их выполняет исполнитель тестов. Это может привести к тому, что модульные тесты пройдут изолированно, но не пройдут в комплекте.
Однако, если ваш тест не использует базу данных, это предупреждение не должно вас беспокоить, и вы можете воспользоваться преимуществами отсутствия необходимости запускать каждый тестовый пример в транзакции.
Вышеупомянутые решения тоже подходят. Но следующее решение также сократит время создания базы данных, если будет большее количество миграций. Во время модульного тестирования запуск syncdb вместо запуска всех южных миграций будет намного быстрее.
SOUTH_TESTS_MIGRATE = False # Чтобы отключить миграции и вместо этого использовать syncdb
Мой веб-хостинг позволяет создавать и удалять базы данных только из своего веб-интерфейса, поэтому при попытке запуска я получал сообщение об ошибке «Получена ошибка при создании тестовой базы данных: в разрешении отказано». 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, поэтому, по крайней мере, я могу контролировать, когда / если я обновлюсь до более новой версии.
Другое решение, о котором не упоминалось: это было легко реализовать, потому что у меня уже есть несколько файлов настроек (для локальных / промежуточных / производственных), которые наследуются от 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
При использовании средства запуска теста носа (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