我想为Django manage.py命令编写一个单元测试,该命令对数据库表进行后端操作。如何从代码中直接调用管理命令?

我不想在操作系统的shell上从tests.py执行命令,因为我不能使用使用manage.py测试设置的测试环境(测试数据库,测试虚拟电子邮件发件箱等…)


测试这些东西的最佳方法是将所需的功能从命令本身提取到独立的函数或类中。它有助于从“命令执行的东西”中抽象出来,并在没有额外需求的情况下编写测试。

但是如果你由于某种原因不能解耦逻辑形式的命令,你可以使用call_command方法从任何代码调用它,就像这样:

from django.core.management import call_command

call_command('my_command', 'foo', bar='baz')

而不是做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)

根据内特的回答,我得到了这个:

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 fords模拟模块,并在测试期间重新布线标准输出,将意味着您也可以从这种技术中获得更多-测试输出,退出条件等。


以下代码:

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文档运行管理命令。


关于call_command的Django文档没有提到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())

使用灵活的参数和捕获的输出运行manage命令的高级方法

argv = self.build_argv(short_dict=kwargs)
cmd = self.run_manage_command_raw(YourManageCommandClass, argv=argv)
# Output is saved cmd.stdout.getvalue() / cmd.stderr.getvalue()

向基本Test类中添加代码

    @classmethod
    def build_argv(cls, *positional, short_names=None, long_names=None, short_dict=None, **long_dict):
        """
        Build argv list which can be provided for manage command "run_from_argv"
        1) positional will be passed first as is
        2) short_names with be passed after with one dash (-) prefix
        3) long_names with be passed after with one tow dashes (--) prefix
        4) short_dict with be passed after with one dash (-) prefix key and next item as value
        5) long_dict with be passed after with two dashes (--) prefix key and next item as value
        """
        argv = [__file__, None] + list(positional)[:]

        for name in short_names or []:
            argv.append(f'-{name}')

        for name in long_names or []:
            argv.append(f'--{name}')

        for name, value in (short_dict or {}).items():
            argv.append(f'-{name}')
            argv.append(str(value))

        for name, value in long_dict.items():
            argv.append(f'--{name}')
            argv.append(str(value))

        return argv

    @classmethod
    def run_manage_command_raw(cls, cmd_class, argv):
        """run any manage.py command as python object"""
        command = cmd_class(stdout=io.StringIO(), stderr=io.StringIO())
        
        with mock.patch('django.core.management.base.connections.close_all'):  
            # patch to prevent closing db connecction
            command.run_from_argv(argv)
        return command