Wie kann ich einen benutzerdefinierten Befehl Django manage.py direkt von einem Testtreiber aus aufrufen?

174

Ich möchte einen Komponententest für einen Django-Befehl "manage.py" schreiben, der eine Backend-Operation für eine Datenbanktabelle ausführt. Wie würde ich den Verwaltungsbefehl direkt aus dem Code aufrufen?

Ich möchte den Befehl nicht in der Shell des Betriebssystems von tests.py ausführen, da ich die mit der Datei "manage.py" eingerichtete Testumgebung (Testdatenbank, Test-Dummy-E-Mail-Postausgang usw.) nicht verwenden kann.

MikeN
quelle

Antworten:

312

Der beste Weg, solche Dinge zu testen - extrahieren Sie die erforderliche Funktionalität aus dem Befehl selbst in eine eigenständige Funktion oder Klasse. Es hilft, von "Befehlsausführungsmaterial" zu abstrahieren und Test ohne zusätzliche Anforderungen zu schreiben.

Wenn Sie den Befehl logic form aus irgendeinem Grund nicht entkoppeln können, können Sie ihn mit der folgenden Methode call_command aus jedem Code aufrufen :

from django.core.management import call_command

call_command('my_command', 'foo', bar='baz')
Alex Koshelev
quelle
19
+1, um die testbare Logik an einer anderen Stelle zu platzieren (Modellmethode? Manager-Methode? Standalone-Funktion?), Damit Sie sich überhaupt nicht mit der call_command-Maschinerie herumschlagen müssen. Erleichtert auch die Wiederverwendung der Funktionalität.
Carl Meyer
36
Selbst wenn Sie die Logik extrahieren, ist diese Funktion immer noch nützlich, um Ihr befehlsspezifisches Verhalten wie die erforderlichen Argumente zu testen und sicherzustellen, dass Ihre Bibliotheksfunktion aufgerufen wird, die die eigentliche Arbeit erledigt.
Igor Sobreira
Der erste Absatz gilt für jede Grenzsituation. Verschieben Sie Ihren eigenen Geschäftslogikcode aus dem Code, der für die Schnittstelle mit etwas wie einem Benutzer eingeschränkt ist. Wenn Sie jedoch die Codezeile schreiben, kann dies zu einem Fehler führen, sodass Tests tatsächlich hinter jede Grenze reichen sollten.
Phlip
Ich denke, dies ist immer noch nützlich call_command('check'), um sicherzustellen, dass die Systemprüfungen in einem Test bestanden werden.
Adam Barnes
22

Anstatt den call_command-Trick auszuführen, können Sie Ihre Aufgabe folgendermaßen ausführen:

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
quelle
10
Warum sollten Sie dies tun, wenn call_command auch die Erfassung von stdin, stdout, stderr vorsieht? Und wann wird in der Dokumentation der richtige Weg angegeben?
Bootscodierer
17
Das ist eine sehr gute Frage. Vor drei Jahren hätte ich vielleicht eine Antwort für dich gehabt;)
Nate
1
Ditto Nate - als seine Antwort das war, was ich vor anderthalb Jahren gefunden habe - habe ich nur darauf aufgebaut ...
Danny Staple
2
Nach dem Graben, aber heute hat mir das geholfen: Ich verwende nicht immer alle Anwendungen meiner Codebasis (abhängig von der verwendeten Django-Site) und call_commandmuss die getestete Anwendung laden INSTALLED_APPS. Ich habe mich dafür entschieden, die App nur zu Testzwecken laden zu müssen und diese zu verwenden.
Mickaël
call_commandist wahrscheinlich das, was die meisten Leute zuerst versuchen sollten. Diese Antwort half mir, ein Problem zu umgehen, bei dem ich Unicode-Tabellennamen an den inspectdbBefehl übergeben musste. Python / Bash interpretierten Befehlszeilenargumente als ASCII, und das bombardierte den get_table_descriptionAnruf tief in Django.
bigh_29
17

den folgenden Code:

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

... entspricht den folgenden im Terminal eingegebenen Befehlen:

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

Siehe Ausführen von Verwaltungsbefehlen aus Django-Dokumenten .

Artur Barseghyan
quelle
14

In der Django-Dokumentation zum Befehl call_command wird nicht erwähnt, outdass an umgeleitet werden muss sys.stdout. Der Beispielcode sollte lauten:

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
quelle
1

Aufbauend auf Nates Antwort habe ich Folgendes:

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

Verwendung:

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

Der Vorteil hierbei ist, dass, wenn Sie zusätzliche Optionen und OptParse verwendet haben, dies für Sie erledigt wird. Es ist nicht ganz perfekt - und es werden noch keine Ausgaben weitergeleitet - aber es wird die Testdatenbank verwenden. Sie können dann auf Datenbankeffekte testen.

Ich bin mir sicher, dass die Verwendung des Micheal Foords-Mock-Moduls und die Neuverdrahtung von stdout für die Dauer eines Tests bedeuten würden, dass Sie auch mehr aus dieser Technik herausholen könnten - testen Sie die Ausgabe, die Ausgangsbedingungen usw.

Danny Staple
quelle
4
Warum sollten Sie sich all diese Mühe machen, anstatt nur call_command zu verwenden?
Bootscodierer