218

I want to write a unit test for a Django manage.py command that does a backend operation on a database table. How would I invoke the management command directly from code?

I don't want to execute the command on the Operating System's shell from tests.py because I can't use the test environment set up using manage.py test (test database, test dummy email outbox, etc...)

MikeN
  • 45,039
  • 49
  • 151
  • 227

6 Answers6

392

The best way to test such things - extract needed functionality from command itself to standalone function or class. It helps to abstract from "command execution stuff" and write test without additional requirements.

But if you by some reason cannot decouple logic form command you can call it from any code using call_command method like this:

from django.core.management import call_command

call_command('my_command', 'foo', bar='baz')
Amir Ali Akbari
  • 5,973
  • 5
  • 35
  • 47
Alex Koshelev
  • 16,879
  • 2
  • 34
  • 28
  • 21
    +1 to putting the testable logic somewhere else (model method? manager method? standalone function?) so you don't need to mess with the call_command machinery at all. Also makes the functionality easier to reuse. – Carl Meyer May 26 '09 at 18:30
  • 40
    Even if you extract the logic this function is still useful to test your command specific behavior, like the required arguments, and to make sure it calls your library function witch does the real work. – Igor Sobreira Dec 05 '12 at 02:05
  • The opening paragraph applies to _any_ boundary situation. Move your own biz logic code out of the code that's constrained to interface with something, such as a user. However, if you write the line of code, it could have a bug, so tests should indeed reach behind any boundary. – Phlip Feb 27 '16 at 16:36
  • I think this is still useful for something like `call_command('check')`, to make sure system checks are passing, in a test. – Adam Barnes Dec 02 '18 at 13:50
25

Rather than do the call_command trick, you can run your task by doing:

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
  • 4,561
  • 2
  • 34
  • 44
  • 11
    Why would you do this when call_command also provides for capturing stdin, stdout, stderr? And when the documentation specifies the right way to do this? – boatcoder Sep 21 '14 at 13:01
  • 22
    That is an extremely good question. Three years ago maybe I would have had an answer for you ;) – Nate Sep 24 '14 at 20:14
  • 1
    Ditto Nate - when his answer was what I found a year and a half ago - I merely built upon it... – Danny Staple Sep 25 '14 at 10:33
  • 3
    Post digging, but today this helped me: I am not always using all the applications of my codebase (depending of the Django site used), and `call_command` needs the tested application to be loaded in `INSTALLED_APPS`. Between having to load the app just for testing purposes and using this, I chose this. – Mickaël Nov 02 '16 at 17:40
  • `call_command` is probably what most people should try first. This answer helped me workaround a problem where I needed to pass unicode table names to the `inspectdb` command. python/bash were interpreting command line args as ascii, and that was bombing the `get_table_description` call deep in django. – bigh_29 Jan 15 '19 at 01:15
  • Reason to use this... My command took a parameter called test, when set the handle function would return a complex object I needed to inspect in my test. So I needed the return of the handle function. – Paul Kenjora Apr 21 '19 at 04:59
21

the following code:

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

...is equal to the following commands typed in terminal:

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

See running management commands from django docs.

radtek
  • 34,210
  • 11
  • 144
  • 111
Artur Barseghyan
  • 12,746
  • 4
  • 52
  • 44
19

The Django documentation on the call_command fails to mention that out must be redirected to sys.stdout. The example code should read:

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())
cezar
  • 11,616
  • 6
  • 48
  • 84
Alan Viars
  • 3,112
  • 31
  • 14
  • 1
    Wow. Thanks for sharing this tip. That's quite an oversight and is [still true as of the 3.2 docs](https://docs.djangoproject.com/en/3.2/topics/testing/tools/#management-commands). I think I'll submit a PR! – Paul Bissex Dec 11 '21 at 22:41
  • I'm confused by this answer, maybe it's out of date. In Django 4.0, output is printed to stdout by default. If you want to capture it, you can use a `StringIO` object and pass it to the `stdout` keyword argument. I don't know what the purpose of `sys.stdout = out` is in this answer. – Flimm Jan 11 '22 at 13:41
  • Also, the docs do mention the `stdout` keyword argument now: https://docs.djangoproject.com/en/stable/topics/testing/tools/#management-commands – Flimm Jan 11 '22 at 13:42
2

Building on Nate's answer I have this:

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

Usage:

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

The advantage here being that if you've used additional options and OptParse, this will sort the out for you. It isn't quite perfect - and it doesn't pipe outputs yet - but it will use the test database. You can then test for database effects.

I am sure use of Micheal Foords mock module and also rewiring stdout for the duration of a test would mean you could get some more out of this technique too - test the output, exit conditions etc.

Danny Staple
  • 7,101
  • 4
  • 43
  • 56
2

The advanced way to run manage command with a flexible arguments and captured output

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()

Add code to your base Test class

    @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
pymen
  • 5,737
  • 44
  • 35