Jak mogę wywołać niestandardowe polecenie Django manage.py bezpośrednio ze sterownika testowego?

174

Chcę napisać test jednostkowy dla polecenia Django manage.py, które wykonuje operację zaplecza na tabeli bazy danych. Jak wywołać polecenie zarządzania bezpośrednio z kodu?

Nie chcę wykonywać polecenia w powłoce systemu operacyjnego z tests.py, ponieważ nie mogę korzystać ze środowiska testowego skonfigurowanego przy użyciu testu manage.py (testowa baza danych, testowa fikcyjna skrzynka nadawcza poczty e-mail itp.)

MikeN
źródło

Odpowiedzi:

312

Najlepszym sposobem na przetestowanie takich rzeczy jest wyodrębnienie potrzebnej funkcjonalności z samego polecenia do samodzielnej funkcji lub klasy. Pomaga to oddzielić się od „rzeczy związanych z wykonywaniem poleceń” i napisać test bez dodatkowych wymagań.

Ale jeśli z jakiegoś powodu nie możesz oddzielić polecenia formularza logicznego, możesz wywołać je z dowolnego kodu za pomocą metody call_command , takiej jak ta:

from django.core.management import call_command

call_command('my_command', 'foo', bar='baz')
Alex Koshelev
źródło
19
+1 do umieszczenia testowalnej logiki w innym miejscu (metoda modelu? Metoda menedżera? Samodzielna funkcja?), Więc nie musisz w ogóle bawić się mechanizmem call_command. Ułatwia również ponowne wykorzystanie tej funkcji.
Carl Meyer
36
Nawet jeśli wyodrębnisz logikę, ta funkcja jest nadal przydatna do testowania zachowania określonego polecenia, takiego jak wymagane argumenty, i upewnienia się, że wywołuje funkcję biblioteki, która wykonuje rzeczywistą pracę.
Igor Sobreira,
Pierwszy akapit dotyczy każdej sytuacji granicznej. Przenieś swój własny kod logiki Biz z kodu, który jest ograniczony do interfejsu z czymś, na przykład użytkownikiem. Jednakże, jeśli napiszesz linię kodu, może to mieć błąd, więc testy powinny rzeczywiście wykraczać poza jakąkolwiek granicę.
Phlip
Myślę, że jest to nadal przydatne do czegoś takiego call_command('check'), jak upewnienie się, że testy systemu przebiegają pomyślnie, w teście.
Adam Barnes
22

Zamiast wykonywać sztuczkę call_command, możesz uruchomić swoje zadanie, wykonując:

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
źródło
10
Dlaczego miałbyś to robić, skoro call_command zapewnia również przechwytywanie stdin, stdout, stderr? A kiedy dokumentacja określa, jak należy to zrobić?
boatcoder
17
To bardzo dobre pytanie. Trzy lata temu może miałbym odpowiedź dla Ciebie;)
Nate
1
Tak samo Nate - kiedy jego odpowiedź była taka, jaką znalazłem półtora roku temu - po prostu zbudowałem na niej ...
Danny Staple
2
Kopanie postów, ale dzisiaj pomogło mi to: nie zawsze używam wszystkich aplikacji z mojej bazy kodu (w zależności od używanej witryny Django) i call_commandpotrzebuję załadowania testowanej aplikacji INSTALLED_APPS. Pomiędzy ładowaniem aplikacji tylko do celów testowych a użyciem tego, wybrałem to.
Mickaël
call_commandjest prawdopodobnie tym, czego większość ludzi powinna spróbować najpierw. Ta odpowiedź pomogła mi obejść problem, w którym musiałem przekazywać nazwy tabel Unicode do inspectdbpolecenia. python / bash interpretował argumenty wiersza poleceń jako ascii, co bombardowało get_table_descriptionwywołanie głęboko w django.
bigh_29
17

następujący kod:

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

... jest równe następującym poleceniom wpisanym w terminalu:

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

Zobacz uruchamianie poleceń zarządzania z dokumentacji django .

Artur Barseghyan
źródło
14

Dokumentacja Django dotycząca call_command nie wspomina, do którego outnależy przekierować sys.stdout. Przykładowy kod powinien brzmieć:

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
źródło
1

Opierając się na odpowiedzi Nate'a, mam to:

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

Stosowanie:

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

Zaletą jest to, że jeśli użyłeś dodatkowych opcji i OptParse, to rozwiąże problem. Nie jest całkiem doskonały - i jeszcze nie przesyła danych wyjściowych - ale użyje testowej bazy danych. Następnie możesz przetestować efekty bazy danych.

Jestem pewien, że użycie makiety modułu Micheal Foords, a także zmiana okablowania standardowego na czas trwania testu, oznaczałoby, że możesz wyciągnąć więcej z tej techniki - przetestuj wyjście, warunki wyjścia itp.

Danny Staple
źródło
4
Dlaczego miałbyś zadawać sobie tyle trudu zamiast po prostu używać call_command?
boatcoder