Django, RQ и FakeRedis

Я часто в своих проектах использую связку Django + RQ вместо Celery. RQ удобный и максимально простой инструмент среди популярных Task Queue решений в экосистеме Python. Пару месяцев назад возникла необходимость тестировать код с сигналами в Django. Схема простая: в ответ на какое-то событие (создание объекта в БД, кастомный сигнал и т.д.) вызывался RQ Job через delay. Дело в том, что такое событие транслировалось ко всем получателям (receivers) как только объект удалялся из базы. Я активный пользователь pytest и создаю промежуточные объекты через стандартные фикстуры. Одно из решений — патчить/mockать job-функции во всех местах, где такие объекты создаются. Но это неудобно и непрактично, с развитием системы количество получателей может расти. Я нашел выход в подмене connection-класса в зависимости от условий. Условие в моём случае это наличие переменной FAKE_REDIS в Django settings.py. Когда FAKE_REDIS=True, то мы заменяем соединение с redis на инстанс класса FakeRedis из пакета fakeredis.

Чтобы этот подход сносно работал необходимо переписать job-декоратор. Вот что получилось у меня:

from rq.decorators import job as rq_job_decorator
from django_rq.queues import get_queue

def async_job(queue_name: str, *args: t.Any, **kwargs: t.Any) -> t.Any:
    """
    The same as RQ's job decorator, but it automatically replaces the
    ``connection`` argument with a fake one if ``settings.FAKE_REDIS`` is set to ``True``.
    """

    class LazyAsyncJob:
        def __init__(self, f: t.Callable[..., t.Any]) -> None:
            self.f = f
            self.job: t.Optional[t.Callable[..., t.Any]] = None

        def setup_connection(self) -> t.Callable[..., t.Any]:
            if self.job:
                return self.job
            if settings.FAKE_REDIS:
                from fakeredis import FakeRedis

                queue = get_queue(queue_name, connection=FakeRedis())  # type: ignore
            else:
                queue = get_queue(queue_name)

            RQ = getattr(settings, 'RQ', {})
            default_result_ttl = RQ.get('DEFAULT_RESULT_TTL')
            if default_result_ttl is not None:
                kwargs.setdefault('result_ttl', default_result_ttl)

            return rq_job_decorator(queue, *args, **kwargs)(self.f)

        def delay(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
            self.job = self.setup_connection()
            return self.job.delay(*args, **kwargs)  # type: ignore

        def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
            self.job = self.setup_connection()
            return self.job(*args, **kwargs)

    return LazyAsyncJob

И все rq таски необходимо декорировать через async_job:

@async_job('default')
def process_image():
    pass

В Django тестах я использую фикстуры pytest:

@pytest.fixture(autouse=True)
def fake_redis(settings: t.Any) -> None:
    settings.FAKE_REDIS = True

Параметр autouse=True нужен для того, чтобы абсолютно во всех тестах использовался fake_redis без явного на то указания.