¿Cómo puedo llamar a un comando personalizado manage.py de Django directamente desde un controlador de prueba?

178

Quiero escribir una prueba unitaria para un comando de Django manage.py que realiza una operación de backend en una tabla de base de datos. ¿Cómo invocaría el comando de administración directamente desde el código?

No quiero ejecutar el comando en el shell del sistema operativo desde tests.py porque no puedo usar el entorno de prueba configurado usando la prueba manage.py (prueba de base de datos, prueba de bandeja de salida de correo electrónico ficticio, etc.)

MikeN
fuente

Respuestas:

320

La mejor manera de probar tales cosas: extraer la funcionalidad necesaria del comando en sí a una función o clase independiente. Ayuda a abstraerse de "cosas de ejecución de comandos" y escribir pruebas sin requisitos adicionales.

Pero si por alguna razón no puede desacoplar el comando de formulario lógico, puede llamarlo desde cualquier código usando el método call_command como este:

from django.core.management import call_command

call_command('my_command', 'foo', bar='baz')
Alex Koshelev
fuente
19
+1 para poner la lógica comprobable en otro lugar (¿método de modelo? ¿Método de administrador? ¿Función independiente?) Para que no tenga que meterse con la maquinaria call_command. También hace que la funcionalidad sea más fácil de reutilizar.
Carl Meyer
37
Incluso si extrae la lógica, esta función sigue siendo útil para probar el comportamiento específico de su comando, como los argumentos requeridos, y para asegurarse de que llama a la función de su biblioteca que hace el trabajo real.
Igor Sobreira
El párrafo inicial se aplica a cualquier situación límite. Saque su propio código de lógica de negocios fuera del código que está limitado a interactuar con algo, como un usuario. Sin embargo, si escribe la línea de código, podría tener un error, por lo que las pruebas deberían llegar más allá de cualquier límite.
Phlip
Creo que esto sigue siendo útil para algo como call_command('check'), para asegurarse de que las comprobaciones del sistema estén pasando en una prueba.
Adam Barnes
22

En lugar de hacer el truco call_command, puede ejecutar su tarea haciendo:

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
fuente
10
¿Por qué haría esto cuando call_command también permite capturar stdin, stdout, stderr? ¿Y cuando la documentación especifica la forma correcta de hacer esto?
Boatcoder
17
Esa es una muy buena pregunta. Hace tres años, tal vez hubiera tenido una respuesta para ti;)
Nate
1
Ditto Nate - cuando su respuesta fue lo que encontré hace un año y medio - simplemente construí sobre eso ...
Danny Staple
2
Después de la excavación, pero hoy esto me ayudó: no siempre estoy usando todas las aplicaciones de mi base de código (dependiendo del sitio de Django utilizado), y call_commandnecesito que se cargue la aplicación probada INSTALLED_APPS. Entre tener que cargar la aplicación solo con fines de prueba y usar esto, elegí esto.
Mickaël
call_commandes probablemente lo que la mayoría de la gente debería probar primero. Esta respuesta me ayudó a solucionar un problema en el que necesitaba pasar nombres de tablas Unicode al inspectdbcomando. python / bash estaban interpretando los argumentos de la línea de comandos como ascii, y eso estaba bombardeando la get_table_descriptionllamada en profundidad en django.
bigh_29
18

el siguiente código:

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

... es igual a los siguientes comandos escritos en la terminal:

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

Consulte ejecutar comandos de administración de los documentos de django .

Artur Barseghyan
fuente
14

La documentación de Django sobre call_command no menciona que se outdebe redirigir a sys.stdout. El código de ejemplo debería leer:

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())
Alan Viars
fuente
1

Sobre la base de la respuesta de Nate, tengo esto:

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

Uso:

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

La ventaja aquí es que si ha utilizado opciones adicionales y OptParse, esto lo resolverá por usted. No es del todo perfecto, y aún no canaliza los resultados, pero usará la base de datos de prueba. A continuación, puede probar los efectos de la base de datos.

Estoy seguro de que el uso del módulo simulado de Micheal Foords y también el recableado de la salida estándar durante la duración de una prueba significaría que también podría sacar más provecho de esta técnica: probar la salida, las condiciones de salida, etc.

Danny Staple
fuente
4
¿Por qué te tomarías todo este problema en lugar de simplemente usar call_command?
Boatcoder