Как я могу вызвать пользовательскую команду Django manage.py прямо из тестового драйвера?

174

Я хочу написать модульный тест для команды Django manage.py, которая выполняет внутреннюю операцию над таблицей базы данных. Как бы я вызвал команду управления прямо из кода?

Я не хочу выполнять команду в оболочке операционной системы из tests.py, потому что не могу использовать тестовую среду, настроенную с использованием теста manage.py (тестовая база данных, тестовая фиктивная электронная почта и т. Д.)

MikeN
источник

Ответы:

312

Лучший способ проверить такие вещи - извлечь необходимую функциональность из самой команды в отдельную функцию или класс. Это помогает абстрагироваться от «материала выполнения команд» и написать тест без дополнительных требований.

Но если вы по какой-то причине не можете отделить команду логической формы, вы можете вызвать ее из любого кода, используя метод call_command, например:

from django.core.management import call_command

call_command('my_command', 'foo', bar='baz')
Алекс Кошелев
источник
19
+1 к тому, чтобы поместить тестируемую логику в другое место (метод модели? Метод менеджера? Автономная функция?), Чтобы вам вообще не приходилось связываться с механизмом call_command. Также облегчает повторное использование функциональности.
Карл Мейер
36
Даже если вы извлекаете логику, эта функция по-прежнему полезна для проверки поведения вашей команды, например, необходимых аргументов, и чтобы убедиться, что она вызывает вашу библиотечную функцию, которая действительно работает.
Игорь Собрейра
Первый абзац применяется к любой граничной ситуации. Удалите свой собственный логический код из кода, ограниченного для взаимодействия с чем-либо, например, с пользователем. Однако, если вы напишите строку кода, это может привести к ошибке, поэтому тесты действительно должны выходить за любую границу.
Флип
Я думаю, что это все еще полезно для чего-то вроде call_command('check'), чтобы убедиться, что системные проверки проходят, в тесте.
Адам Барнс
22

Вместо того, чтобы делать трюк call_command, вы можете запустить вашу задачу, выполнив:

from myapp.management.commands import my_management_task
cmd = my_management_task.Command()
opts = {} # kwargs for your command -- lets you override stuff for testing...
cmd.handle_noargs(**opts)
Nate
источник
10
Зачем вам это делать, если call_command также обеспечивает захват stdin, stdout, stderr? А когда в документации указан правильный способ сделать это?
Боевой кодер
17
Это очень хороший вопрос. Три года назад, возможно, у меня был бы ответ для вас;)
Нейт,
1
То же самое и Нейт - когда его ответ был тем, что я нашел полтора года назад - я просто опирался на него ...
Дэнни Стейпл
2
После копания, но сегодня это мне помогло: я не всегда использую все приложения из моей кодовой базы (в зависимости от используемого сайта Django), и мне call_commandнужно загрузить проверенное приложение INSTALLED_APPS. Между тем, чтобы загрузить приложение только для тестирования и использовать его, я выбрал это.
Микаэль
call_commandвероятно, это то, что большинство людей должны попробовать в первую очередь. Этот ответ помог мне обойти проблему, когда мне нужно было передать имена таблиц Unicode в inspectdbкоманду. python / bash интерпретировал аргументы командной строки как ascii, и это было get_table_descriptionвызовом глубоко в django.
bigh_29
17

следующий код:

from django.core.management import call_command
call_command('collectstatic', verbosity=3, interactive=False)
call_command('migrate', 'myapp', verbosity=3, interactive=False)

... равнозначно следующим командам, набранным в терминале:

$ ./manage.py collectstatic --noinput -v 3
$ ./manage.py migrate myapp --noinput -v 3

Смотрите команды управления из документации django .

Артур Барсегян
источник
14

В документации по Django для call_command не упоминается, что она outдолжна быть перенаправлена sys.stdout. Код примера должен выглядеть так:

from django.core.management import call_command
from django.test import TestCase
from django.utils.six import StringIO
import sys

class ClosepollTest(TestCase):
    def test_command_output(self):
        out = StringIO()
        sys.stdout = out
        call_command('closepoll', stdout=out)
        self.assertIn('Expected output', out.getvalue())
Алан Виарс
источник
1

Опираясь на ответ Нейта, у меня есть это:

def make_test_wrapper_for(command_module):
    def _run_cmd_with(*args):
        """Run the possibly_add_alert command with the supplied arguments"""
        cmd = command_module.Command()
        (opts, args) = OptionParser(option_list=cmd.option_list).parse_args(list(args))
        cmd.handle(*args, **vars(opts))
    return _run_cmd_with

Использование:

from myapp.management import mycommand
cmd_runner = make_test_wrapper_for(mycommand)
cmd_runner("foo", "bar")

Преимущество здесь в том, что если вы использовали дополнительные опции и OptParse, это поможет вам. Он не совсем совершенен - ​​и пока не передает результаты - но будет использовать тестовую базу данных. Затем вы можете проверить эффекты базы данных.

Я уверен, что использование макетного модуля Micheal Foords, а также переподключение стандартного вывода на время теста означало бы, что вы также можете получить больше от этой техники - проверить выход, условия выхода и т. Д.

Дэнни Стейпл
источник
4
Зачем вам идти на все эти неприятности вместо того, чтобы просто использовать call_command?
Боевой кодер