me.neoascetic

Django persistent DB runner

Как заставить django использовать существующую БД для тестов и не уничтожать ее после завершения? Простое решение через манкипатчинг.

Как известно, для тестов Django создает новую базу данных (по умолчанию - с префиксом test_) со структурой, основанной на моделях (иначе говоря, прогоняет syncdb, и migrate, если используется south).

Если для тестов необходимы какие-то данные в базе, можно использовать фикстуры (fixtures), которые представляют из себя предварительно сериализованные данные в форматах YAML/XML/JSON на выбор, что делает их СУБД-независимыми - при запуске тестов они загружаются в новосозданную БД сами (когда используется django.utils.unittests.TestCase в качестве базового класса) или с помощью директивы (если используется стандартный TestCase). Подробно об этом - здесь.

И все бы хорошо, если бы не скорость всего этого дела, когда необходимо прогнать небольшой тест, который никоим образом не модифицирует данные - в нашем случае объем данных для инициализации был слишком большим. Соотношение загрузка/выполнение порой достигало 10/1, что весьма расстраивало. Поэтому мы решили заюзать постоянную (persistent) базу данных, дабы вообще отказаться от создания новой. То есть подразумевается, что система уже работает, а тестовая база - клон боевой. При поиске подходящего решения был найден только Persistent DB Runner из django_test_utils, но он не работал, как надо, его вроде даже запустить не удалось, уже и не помню, почему.

Поэтому принято было решение сделать по-быстрому свой TEST_RUNNER. TEST_RUNNER - это настройка, в которой в текстовом виде нужно указать путь к классу, который бы настраивал окружение для тестов.

В стандартном джанговском TEST_RUNNER (DjangoTestSuiteRunner) есть, помимо других, два интересующих нас метода - setup_databases и teardown_databases, что они делают, думаю, понятно из названия. Итак, дело, казалось бы, за малым - наследовать от DjangoTestSuiteRunner, переопределить соответствующие методы - и вуаля!

Но не все так просто - методы это не маленькие, да и за создание/разрушение тестовой базы непосредственно они не отвечают - в нужных местах вызывается create_test_db/destroy_test_db, которые являются методами классов, реализующих “адаптеры” для указанных в настройках баз данных (их ведь может быть несколько, к тому же могут быть разных видов) - все они наследуют от BaseDatabaseCreation, в котором нужные методы и определены. То есть - переопределить нужно именно методы соответствующих классов, а не DjangoTestSuiteRunner.

Но как все сделать элегантно и кратко, если в настройках мы можем указывать только TEST_RUNNER, который уже потом сам подтягивает потомков BaseDatabaseCreation? Переопределять-таки эти методы, переписывать весь код (ибо super вызвать не получится), и указывать, чтобы в качестве базового для БД использовался наш класс? Но и тут не все так просто - явного создания экземпляра класса в этих методах нет, а используется итерируемый объект connections, который является экземпляром класса ConnectionHandler, который создаётся в django.db.__init__.py, который содержит содержит список всех используемых БД, каждая из которых может использовать свой движок, который наследует от BaseDatabaseCreation… в доме который построил Джек. Короче, гляньте код, все сразу станет понятно.

Итак, в итоге было решено использовать манкипатчинг. Так как setup_databases и teardown_databases нам, оказывается, не нужны, но указать в настройках мы можем только TEST_RUNNER, лучшим местом для манки, мать его, патчинга, стал метод __init__ нашего наследника DjangoTestSuiteRunner, потому как подмена методов нам нужна только при выполнении тестов, а не глобально.

Итак, результирующий код:

class PersistentDBRunner(DjangoTestSuiteRunner):

    def __init__(self, *args, **kwargs):
        super(PersistentDBRunner, self).__init__(*args, **kwargs)

        def _create_test_db(self, *args, **kwargs):
            test_database_name = self._get_test_db_name()
            self.connection.close()
            self.connection.settings_dict["NAME"] = test_database_name
            cursor = self.connection.cursor()
            return test_database_name

        def _destroy_test_db(self, old_db_name, *args, **kwargs):
            self.connection.close()
            self.connection.settings_dict['NAME'] = old_db_name

        creation.BaseDatabaseCreation.create_test_db = _create_test_db
        creation.BaseDatabaseCreation.destroy_test_db = _destroy_test_db

Вот и все. Помещаем класс где-нибудь, указываем в TEST_RUNNER путь к нему - и радуемся тому, что ./manage.py test не создаёт и не убивает тестовую базу. Разумеется, чтобы все работало, её предварительно нужно создать и забить данными.