Рубрики
Без рубрики

Как проверить сигналы Django, как Pro

Автор оригинала: FreeCodeCapm Team.

Хаки Бенита

Для лучшего опыта чтения проверить Эта статья на моем сайте Отказ

Django сигналы чрезвычайно полезны для отделения модулей. Они позволяют низкоуровневым приложение Django отправлять события для других приложений для обработки без создания прямой зависимости.

Сигналы легко настроить, но сложнее тестировать. Таким образом, в этой статье я собираюсь пройти вас через реализацию контекстно-менеджера по тестированию сигналов Django, шаг за шагом.

Использование случая

Допустим, у вас есть платежный модуль с функцией заряда. (Я …| Напишите много о платежах , поэтому я знаю это дело хорошо.) После того, как заряд сделан, вы хотите увеличить счетную счетность.

Как бы это выглядело использовать сигналы?

Сначала определите сигнал:

# signals.py
from django.dispatch import Signal
charge_completed = Signal(providing_args=['total'])

Затем отправьте сигнал, когда заряд успешно завершится:

# payment.py
from .signals import charge_completed
@classmethoddef process_charge(cls, total):
    # Process charge…
    if success:        charge_completed.send_robust(            sender=cls,            total=total,        )

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

# summary.py
from django.dispatch import receiver
from .signals import charge_completed
@receiver(charge_completed)def increment_total_charges(sender, total, **kwargs):    total_charges += total

Платежный модуль не должен знать сводный модуль или любой другой модуль, завершенный завершенные заряды. Вы можете добавить много приемников, не модифицируя модуль оплаты Отказ

Например, следующие являются хорошими кандидатами для приемников:

  • Обновите статус транзакции.
  • Отправьте уведомление по электронной почте пользователю.
  • Обновите последнюю использованную дату кредитной карты.

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

Теперь, когда вы получили основные основы, давайте напишем тест на Process_Chare Отказ Вы хотите убедиться, что сигнал отправляется с правильными аргументами, когда заряд завершится успешно.

Лучший способ проверить, если сигнал был отправлен, это подключиться к нему:

# test.py
from django.test import TestCase
from .payment import chargefrom .signals import charge_completed
class TestCharge(TestCase):
    def test_should_send_signal_when_charge_succeeds(self):        self.signal_was_called = False        self.total = None
        def handler(sender, total, **kwargs):            self.signal_was_called = True            self.total = total
        charge_completed.connect(handler)
        charge(100)
        self.assertTrue(self.signal_was_called)        self.assertEqual(self.total, 100)
        charge_completed.disconnect(handler)

Мы создаем обработчик, подключаем к сигналу, выполняйте функцию и проверьте args.

Мы используем Я внутри обработчика, чтобы создать закрытие. Если бы мы не использовали Я Функция обработчика обновит переменные в его локальном объеме, и у нас не будет доступа к ним. Мы пересматриваем это позже.

Давайте добавим тест на Убедитесь, что сигнал не вызывается, если заряд не удалась :

def test_should_not_send_signal_when_charge_failed(self):    self.signal_was_called = False
    def handler(sender, total, **kwargs):        self.signal_was_called = True
    charge_completed.connect(handler)
    charge(-1)
    self.assertFalse(self.signal_was_called)
    charge_completed.disconnect(handler)

Это работает, но это Много котельной! Там должен быть лучший способ.

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

Давайте сломаемся, что мы сделали до сих пор:

  1. Подключите сигнал к некоторым обработчике.
  2. Запустите тестовый код и сохраните аргументы, переданные обработчику.
  3. Отсоедините обработчик от сигнала.

Этот шаблон звучит знакомо …

Давайте посмотрим на что a (файл) Открытый контекст менеджер делает:

  1. Откройте файл.
  2. Процесс файл.
  3. Закройте файл.

И а Менеджер контекста транзакции базы данных :

  1. Открытая транзакция.
  2. Выполнить некоторые операции.
  3. Закрыть транзакцию (Commit/Rollback).

Похоже …| Менеджер контекста может работать для сигналов, а также Отказ

Прежде чем начать, подумайте, как вы хотите использовать контекстный менеджер по тестированию сигналов:

with CatchSignal(charge_completed) as signal_args:    charge(100)
self.assertEqual(signal_args.total, 100)

Приятно, давайте попробуем:

class CatchSignal:    def __init__(self, signal):        self.signal = signal        self.signal_kwargs = {}
        def handler(sender, **kwargs):            self.signal_kwrags.update(kwargs)
        self.handler = handler
    def __enter__(self):        self.signal.connect(self.handler)        return self.signal_kwrags
    def __exit__(self, exc_type, exc_value, tb):        self.signal.disconnect(self.handler)

Что у нас здесь:

  • Вы инициализировали контекст с сигналом, который вы хотите «поймать».
  • Контекст создает функцию обработчика для сохранения аргументов, отправленных сигналом.
  • Вы создаете закрытие, обновляя существующий объект ( Signal_kwargs ) на Я Отказ
  • Вы подключаете обработчик к сигналу.
  • Некоторая обработка сделана (по тесту) между __enter__ и __exit__ Отказ
  • Вы отсоедините обработчик от сигнала.

Давайте использовать Context Manager для проверки функции заряда:

def test_should_send_signal_when_charge_succeeds(self):    with CatchSignal(charge_completed) as signal_args:        charge(100)    self.assertEqual(signal_args['total'], 100)

Это лучше, но Как выглядит отрицательный тест?

def test_should_not_send_signal_when_charge_failed(self):    with CatchSignal(signal) as signal_args:        charge(100)    self.assertEqual(signal_args, {})

Як, это плохо.

Давайте сделаем еще один взгляд на обработчик:

  • Мы хотим убедиться, что функция обработчика была вызвана.
  • Мы хотим проверить args, отправленные в функцию обработчика.

Подожди … Я уже знаю эту функцию!

Войти в список

Давайте заменим наш обработчик издевательства:

from unittest import mock
class CatchSignal:    def __init__(self, signal):        self.signal = signal        self.handler = mock.Mock()
    def __enter__(self):        self.signal.connect(self.handler)        return self.handler
    def __exit__(self, exc_type, exc_value, tb):        self.signal.disconnect(self.handler)

И тесты:

def test_should_send_signal_when_charge_succeeds(self):    with CatchSignal(charge_completed) as handler:        charge(100)    handler.assert_called_once_with(        total=100,        sender=mock.ANY,        signal=charge_completed,    )
def test_should_not_send_signal_when_charge_failed(self):    with CatchSignal(charge_completed) as handler:        charge(-1)        handler.assert_not_called()

Намного лучше!

Вы использовали издевание именно то, что он должен использоваться, и вам не нужно беспокоиться о объеме и закрытии.

Теперь, когда у вас есть эта работа, Можете ли вы сделать это еще лучше?

Введите contextlib

У Python есть коммунальный модуль для обработки контекстных менеджеров, называемых Contextlib Отказ

Давайте переписать наш контекст, используя Contextlib :

from unittest import mockfrom contextlib import contextmanager
@contextmanagerdef catch_signal(signal):    """Catch django signal and return the mocked call."""    handler = mock.Mock()    signal.connect(handler)    yield handler    signal.disconnect(handler)

Мне нравится этот подход лучше, потому что легче следовать:

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

И вот это – 4 строки кода, чтобы управлять им все! Выгода!