тести django unit без db


126

Чи є можливість записати django unittests без встановлення db? Я хочу перевірити ділову логіку, яка не вимагає налаштування db. І хоча це швидко налаштовує db, я справді не потребує цього в деяких ситуаціях.


Мені цікаво, чи це насправді має значення. ДБ зберігається в пам'яті + якщо у вас немає жодної моделі, з db нічого не виконується. Тож якщо вам це не потрібно, не встановлюйте моделі.
Торстен Енгельбрехт

3
У мене є моделі, але для цих тестів вони не стосуються. І db не зберігається в пам'яті, а створюється в mysql, однак спеціально для цієї мети. Не те, що я хочу цього .. Можливо, я міг би налаштувати django для використання db-пам'яті для тестування. Чи знаєте ви, як це зробити?
paweloque

О, мені шкода. Бази даних в пам'яті - це лише випадок, коли ви використовуєте базу даних SQLite. Крім цього, я не бачу способу уникнути створення тестового db. У цьому документі нічого немає + я ніколи не відчував необхідності цього уникати.
Торстен Енгельбрехт

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'

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


2
Ця помилка виникає, коли у вас є тести, які потребують транзакцій із базами даних. Очевидно, якщо у вас немає БД, ви не зможете запускати ці тести. Вам слід запустити свої тести окремо. Якщо ви просто запустите свій тест за допомогою python Manag.py test --settings = new_settings.py, він запустить цілу купу інших тестів з інших додатків, які можуть потребувати бази даних.
mohi666

5
Зауважте, що вам потрібно буде розширити SimpleTestCase замість TestCase для ваших тестових класів. TestCase очікує на базу даних.
Бен Робертс

9
Якщо ви не хочете використовувати новий файл налаштувань, ви можете вказати новий TestRunner у командному рядку за допомогою --testrunnerпараметра.
Бран Хендлі

26
Чудова відповідь !! У django 1.8 з django.test.simple import було змінено DjangoTestSuiteRunner на django.test.runner import DiscoverRunner Сподіваюся, що хтось допомагає!
Джош Браун

2
У Джанго 1.8 і вище можна зробити невелику корекцію вищевказаного коду. Заяву про імпорт можна змінити на: з 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
Це може зробити запущені тести більш ефективними, але зауважте, що тестовий бігун все ще створює тестові бази даних при ініціалізації.
monkut

6
Настільки простіша, що обрана відповідь. Дуже дякую!
KFunk

1
@monkut Ні ... якщо у вас є лише клас SimpleTestCase, тестовий бігун нічого не запускає, дивіться цей проект .
Клаудіо Сантос

Django все ще намагатиметься створити тестовий БД, навіть якщо ви використовуєте лише SimpleTestCase. Дивіться це питання .
Marko Prcać

використання SimpleTestCase точно працює для тестування корисних методів або фрагментів і не використовує і не створює тестовий db. Саме те, що мені потрібно!
Хантер

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

8

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

Перше моє додавання перевіряє, чи потрібна настройка db, і чи дозволяє нормальна setup_databasesфункціональність починати, якщо необхідний db. Моє друге доповнення дозволяє нормальному teardown_databasesзапуску, якщо setup_databasesметоду було дозволено запустити.

Мій код передбачає, що будь-який TestCase, який успадковує django.test.TransactionTestCase(і таким чином django.test.TestCase), потребує налаштування бази даних. Я зробив це припущення, оскільки документи Джанго кажуть:

Якщо вам потрібна будь-яка інша, більш складна і важка для Django функція, наприклад ... Тестування або використання ORM ..., тоді вам слід використовувати TransactionTestCase або TestCase.

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

mysite / script / 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.


@Cesar має рацію. Після випадкового запуску ./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

Я запустив тести, використовуючи 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

ПРИМІТКА. Якщо ви видалите з'єднання за замовчуванням зі списку з'єднань, ви не зможете використовувати моделі Django або інші функції, які зазвичай використовують базу даних (очевидно, ми не спілкуємося з базою даних, але Django перевіряє різні функції, які підтримує БД) . Також здається, що connection._connections більше не підтримує __getitem__. Використовуйте connections._connections.defaultдля доступу до об'єкта.
the_drow

2

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

Використання unittest.TestCase дозволяє уникнути витрат на виконання кожного тесту в транзакції та промивання бази даних, але якщо ваші тести взаємодіють із базою даних, їх поведінка буде змінюватися залежно від порядку, який виконує їх тестувальник. Це може призвести до одиничних тестів, які проходять при запуску в ізоляції, але провалюються під час запуску в наборі.

Однак якщо ваш тест не використовує базу даних, це попередження не повинно вас стосуватись, і ви можете скористатися перевагами від того, що не потрібно запускати кожен тестовий випадок в транзакції.


Схоже, це все ще створює і знищує db, лише різниця полягає в тому, що він не запускає тест в транзакції і не змиває db.
Cam Rail

0

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

SOUTH_TESTS_MIGRATE = False # Щоб відключити міграцію та використовувати замість цього syncdb


0

Мій веб-хост дозволяє лише створювати та видаляти бази даних з їх веб-інтерфейсу, тому я отримував помилку "Помилка створення тестової бази даних: Дозвіл відхилено" при спробі запуску python manage.py test.

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

Що я в кінцевому підсумку робив, - це змінити код Django у ... / django / db / backends / create.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, тому принаймні я маю контроль над тим, коли / якщо я перейду до нової версії.


0

Ще одне рішення, яке не згадано: це було легко реалізувати для мене, оскільки у мене вже є декілька файлів налаштувань (для локальних / постановочних / виробничих), які успадковуються від base.py. Отже, на відміну від інших людей, мені не довелося перезаписувати DATABASES ['default'], оскільки DATABASES не встановлено в base.py

SimpleTestCase все ж намагався підключитися до моєї тестової бази даних та запустити міграцію. Коли я створив файл config / settings / test.py, який нічого не встановлював DATABASES, тоді мої модульні тести проходили без нього. Це дозволило мені використовувати моделі, які мали зовнішній ключ та унікальні поля обмеження. (Зворотний пошук зовнішнього ключа, для якого потрібен пошук db, не вдається.)

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

Використовуючи нос-тест-бігун (джанго-ніс), ви можете зробити щось подібне:

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.