Как да тествате Django Signals като професионалист

Приемник на сигнал Радвам се, че не трябва да тествам

За по-добро преживяване при четене, разгледайте тази статия на моя уебсайт.

Сигналите на Django са изключително полезни за отделяне на модули. Те позволяват на приложението Django от ниско ниво да изпраща събития за други приложения, които да обработват, без да създават пряка зависимост.

Сигналите се настройват лесно, но по-трудно се тестват. Така че в тази статия ще ви преведа през прилагането на контекст мениджър за тестване на сигнали на Django, стъпка по стъпка.

Случаят за употреба

Да речем, че имате платежен модул с функция за таксуване. (Пиша много за плащания, така че знам добре този случай на използване.) След като бъде извършено таксуване, вие искате да увеличите брояча на общите такси.

Как би изглеждало използването на сигнали?

Първо определете сигнала:

# signals.py
от django.dispatch Сигнал за импортиране
charge_completed = Сигнал (предоставяне_args = ['общо'])

След това изпратете сигнала, когато зареждането завърши успешно:

# Payment.py
от .signals импортиране на такса_комплектиран
@classmethod
дефиниране__процес (cls, общо):
    # Зареждане на процеса ...
    ако успех:
        charge_completed.send_robust (
            подател = CLS,
            общо = общо
        )

Различно приложение, като например обобщено приложение, може да свърже обработващо устройство, което увеличава брояча на общите такси:

# sum.py
от приемник за внос на django.dispatch
от .signals импортиране на такса_комплектиран
@receiver (charge_completed)
def increment_total_charges (подател, общо, ** kwargs):
    total_charges + = общо

Модулът за плащане не трябва да знае обобщения модул или който и да е друг модул, обработващ завършени такси. Можете да добавите много приемници, без да променяте модула за плащане.

Например следните добри кандидати за приемници:

  • Актуализирайте състоянието на транзакцията.
  • Изпратете известие по имейл на потребителя.
  • Актуализирайте последната използвана дата на кредитната карта.

Тестване на сигнали

Сега, когато сте обхванали основите, нека пишем тест за process_charge. Искате да се уверите, че сигналът е изпратен с правилните аргументи, когато зареждането приключи успешно.

Най-добрият начин да тествате дали е изпратен сигнал е да се свържете към него:

# test.py
от django.test импортиране TestCase
от такса за внос на плащане
от .signals импортиране на такса_комплект
клас TestCharge (TestCase):
    def test_should_send_signal_when_charge_succeeds (self):
        self.signal_was_called = Грешно
        self.total = Няма
        def handler (подател, общо, ** kwargs):
            self.signal_was_called = Вярно
            self.total = общо
        charge_completed.connect (манипулатор)
        заряд (100)
        self.assertTrue (self.signal_was_called)
        self.assertEqual (self.total, 100)
        charge_completed.disconnect (манипулатор)

Създаваме манипулатор, свързваме се със сигнала, изпълняваме функцията и проверяваме арговете.

Използваме self вътре в манипулатора за създаване на затваряне. Ако не бяхме използвали самостоятелно функцията за обработване ще актуализира променливите в локалния й обхват и няма да имаме достъп до тях. Ще разгледаме това по-късно.

Нека добавим тест, за да се уверим, че сигналът не е извикан, ако зареждането е неуспешно:

def test_should_not_send_signal_when_charge_failed (self):
    self.signal_was_called = Грешно
    def handler (подател, общо, ** kwargs):
        self.signal_was_called = Вярно
    charge_completed.connect (манипулатор)
    заряд (1)
    self.assertFalse (self.signal_was_called)
    charge_completed.disconnect (манипулатор)

Това работи, но е много котлон! Трябва да има по-добър начин.

Въведете контекстния мениджър

Нека разбием това, което направихме досега:

  1. Свържете сигнал към някой манипулатор.
  2. Пуснете тестовия код и запишете аргументите, предадени на обработващия.
  3. Изключете манипулатора от сигнала.

Този модел звучи познато ...

Нека да разгледаме какво прави (файл) отворен контекст мениджър:

  1. Отворете файл.
  2. Обработете файла.
  3. Затворете файла.

И мениджър на контекста за транзакция на база данни:

  1. Отворена транзакция.
  2. Изпълнете някои операции.
  3. Затваряне на транзакция (ангажиране / отмяна).

Изглежда, че контекстният мениджър може да работи и за сигнали.

Преди да започнете, помислете как искате да използвате контекстния мениджър за тестване на сигнали:

с CatchSignal (такса_комплектиран) като сигнал_арг:
    заряд (100)
self.assertEqual (signal_args.total, 100)

Хубаво, нека опитаме:

клас CatchSignal:
    def __init __ (самостоятелно, сигнал):
        self.signal = сигнал
        self.signal_kwargs = {}
        def handler (подател, ** kwargs):
            self.signal_kwrags.update (kwargs)
        self.handler = манипулатор
    def __enter __ (самостоятелно):
        self.signal.connect (self.handler)
        върнете self.signal_kwrags
    def __exit __ (самостоятелно, exc_type, exc_value, tb):
        self.signal.disconnect (self.handler)

Какво имаме тук:

  • Инициализирате контекста със сигнала, който искате да „уловите“.
  • Контекстът създава функция за обработка, за да запише аргументите, изпратени от сигнала.
  • Вие създавате затваряне чрез актуализиране на съществуващ обект (signal_kwargs) на самостоятелно.
  • Свързвате манипулатора към сигнала.
  • Някои обработки се извършват (чрез теста) между __enter__ и __exit__.
  • Вие изключвате манипулатора от сигнала.

Нека използваме контекстния мениджър за тестване на функцията за таксуване:

def test_should_send_signal_when_charge_succeeds (self):
    с CatchSignal (такса_комплектиран) като сигнал_арг:
        заряд (100)
    self.assertEqual (signal_args ['общо'], 100)

Това е по-добре, но как би изглеждал отрицателният тест?

def test_should_not_send_signal_when_charge_failed (self):
    с CatchSignal (сигнал) като signal_args:
        заряд (100)
    self.assertEqual (signal_args, {})

Да, това е лошо.

Да вземем друг поглед към водача:

  • Искаме да се уверим, че функцията за обработка е била извикана.
  • Искаме да тестваме аргументите, изпратени към функцията за обработка.

Чакайте ... вече знам тази функция!

Въведете макет

Нека да заменим нашия манипулатор с Mock:

от unittest макет за внос
клас CatchSignal:
    def __init __ (самостоятелно, сигнал):
        self.signal = сигнал
        self.handler = mock.Mock ()
    def __enter __ (самостоятелно):
        self.signal.connect (self.handler)
        върнете самостоятелно ръководител
    def __exit __ (самостоятелно, exc_type, exc_value, tb):
        self.signal.disconnect (self.handler)

И тестовете:

def test_should_send_signal_when_charge_succeeds (self):
    с CatchSignal (такса_комплект) като обработващ:
        заряд (100)
    handler.assert_called_once_with (
        общо = 100,
        подател = mock.ANY,
        сигнал = charge_completed,
    )
def test_should_not_send_signal_when_charge_failed (self):
    с CatchSignal (такса_комплект) като обработващ:
        заряд (1)
    handler.assert_not_called ()

Много по-добре!

Използвахте макета точно за това, за което трябва да се използва и няма нужда да се притеснявате за обхвата и затварянето.

Сега, когато работиш, можеш ли да го направиш още по-добър?

Въведете contextlib

Python има полезен модул за работа с контекстни мениджъри, наречен contextlib.

Нека да пренапишем нашия контекст с помощта на contextlib:

от unittest макет за внос
от импортиране на контекстно управление
@contextmanager
def catch_signal (сигнал):
    "" "Хванете сигнал за django и върнете изписаното повикване." ""
    handler = mock.Mock ()
    signal.connect (манипулатор)
    производител на добив
    signal.disconnect (манипулатор)

Този подход ми харесва по-добре, защото е по-лесно да се следва:

  • Добивът показва ясно къде се изпълнява тестовият код.
  • Няма нужда да запазвате обекти самостоятелно, защото кодът за настройка (влизане и излизане) е в същия обхват.

И това е всичко - 4 реда код, за да управлява всички тях! Печалба!