如何直接从测试驱动程序调用自定义 Django manage.py 命令?

Posted

技术标签:

【中文标题】如何直接从测试驱动程序调用自定义 Django manage.py 命令?【英文标题】:How can I call a custom Django manage.py command directly from a test driver? 【发布时间】:2010-10-28 19:04:48 【问题描述】:

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

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

【问题讨论】:

【参考方案1】:

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

但是如果你由于某种原因不能解耦逻辑表单命令,你可以使用 call_command 方法从任何代码中调用它,如下所示:

from django.core.management import call_command

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

【讨论】:

+1 将可测试的逻辑放在其他地方(模型方法?管理方法?独立函数?)所以你根本不需要弄乱 call_command 机器。还使功能更易于重用。 即使你提取了逻辑,这个函数仍然可以用来测试你的命令特定行为,比如所需的参数,并确保它调用你的库函数来完成真正的工作。 开头段落适用于任何边界情况。将您自己的 biz 逻辑代码从受限于与某物(例如用户)交互的代码中移出。但是,如果您编写这行代码,它可能会出现错误,因此测试确实应该超出任何边界。 我认为这对于像call_command('check') 这样的东西仍然有用,以确保系统检查在测试中通过。【参考方案2】:

您可以通过执行以下操作来运行您的任务,而不是使用 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)

【讨论】:

当 call_command 还提供捕获标准输入、标准输出、标准错误时,为什么要这样做?什么时候文档指定了正确的方法呢? 这是一个非常好的问题。三年前,也许我会给你一个答案;) Ditto Nate - 当他的答案是我一年半前发现的时候 - 我只是建立在它之上...... 后挖掘,但今天这对我有帮助:我并不总是使用我的代码库的所有应用程序(取决于所使用的 Django 站点),call_command 需要在 @ 中加载经过测试的应用程序987654323@。在仅为测试目的加载应用程序和使用它之间,我选择了这个。 call_command 可能是大多数人应该首先尝试的。这个答案帮助我解决了需要将 unicode 表名传递给 inspectdb 命令的问题。 python/bash 将命令行 args 解释为 ascii,这正在炸毁 django 深处的 get_table_description 调用。【参考方案3】:

基于 Nate 的回答,我有这个:

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

【讨论】:

你为什么要解决所有这些麻烦而不是只使用 call_command?【参考方案4】:

以下代码:

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

见running management commands from django docs。

【讨论】:

【参考方案5】:

Django documentation on the 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())

【讨论】:

哇。感谢分享这个技巧。这是一个很大的疏忽,是still true as of the 3.2 docs。我想我会提交 PR! 我对这个答案感到困惑,也许它已经过时了。在 Django 4.0 中,默认情况下将输出打印到标准输出。如果要捕获它,可以使用StringIO 对象并将其传递给stdout 关键字参数。我不知道sys.stdout = out 在这个答案中的目的是什么。 此外,文档现在确实提到了 stdout 关键字参数:docs.djangoproject.com/en/stable/topics/testing/tools/…【参考方案6】:

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

argv = self.build_argv(short_dict=kwargs)
cmd = self.run_manage_command_raw(YourManageCommandClass, argv=argv)

将代码添加到您的基本测试类

    @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

【讨论】:

以上是关于如何直接从测试驱动程序调用自定义 Django manage.py 命令?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Django 中测试自定义模板标签?

如何从 python 代码中调用 django 模板标签?

Django 测试数据库未与自定义测试运行程序一起使用

Python 3 Django Rest Framework - 如何向这个 M-1-M 模型结构添加自定义管理器?

如何测试自定义 django-admin 命令

在matlab中怎么直接调用函数