Як ви перевіряєте завдання із селери?


Відповіді:


61

Можна тестувати завдання синхронно, використовуючи будь-яку вкладку одиничних тестів. Я нормально роблю 2 різні тестові сесії, коли працюю із завданнями із селери. Перший (як я пропоную нижче) повністю синхронний і повинен бути таким, який гарантує, що алгоритм робить те, що повинен робити. Другий сеанс використовує всю систему (включаючи брокера) і гарантує, що у мене немає проблем із серіалізацією чи будь-якою іншою проблемою з розподілом, комунікацією.

Так:

from celery import Celery

celery = Celery()

@celery.task
def add(x, y):
    return x + y

І ваш тест:

from nose.tools import eq_

def test_add_task():
    rst = add.apply(args=(4, 4)).get()
    eq_(rst, 8)

Сподіваюся, що це допомагає!


1
Це працює за винятком завдань, які використовують HttpDispatchTask - docs.celeryproject.org/en/latest/userguide/remote-tasks.html, де мені потрібно встановити celery.conf.CELERY_ALWAYS_EAGER = Істинно, але навіть якщо також встановити celery.conf.CELERY_IMPORTS = ('celery.task.http') тест не вдається з NotRegistered: celery.task.http.HttpDispatchTask
davidmytton

Дивно, ви впевнені, що у вас немає проблем з імпортом? Цей тест працює (зауважте, що я підробляю відповідь, тому він повертає те, що очікує селера). Також модулі, визначені в CELERY_IMPORTS, будуть імпортовані під час ініціалізації працівників , щоб уникнути цього, я пропоную вам зателефонувати celery.loader.import_default_modules().
FlaPer87

Я б також запропонував вам поглянути тут . Він знущається над запитом http. Не знаю, чи допомагає це, я думаю, ви хочете перевірити працюючу службу, чи не так?
FlaPer87

52

Я використовую це:

with mock.patch('celeryconfig.CELERY_ALWAYS_EAGER', True, create=True):
    ...

Документи: http://docs.celeryproject.org/en/3.1/configuration.html#celery-always-eager

CELERY_ALWAYS_EAGER дозволяє виконувати завдання синхронно, і вам не потрібен сервер із селери.


1
Я думаю, що це застаріло - я розумію ImportError: No module named celeryconfig.
Daenyth

7
Я вважаю, що вище припускає, що модуль celeryconfig.pyіснує в пакеті. Дивіться docs.celeryproject.org/en/latest/getting-started/… .
Каміль Сінді

1
Я знаю, що це старе, але чи можете ви надати повний приклад того, як запускати завдання addз питання ОП в TestCaseкласі?
Kruupös

@ MaxChrétien Вибачте, я не можу навести повний приклад, оскільки я вже не використовую селеру. Ви можете відредагувати моє запитання, якщо у вас достатньо балів репутації. Якщо у вас недостатньо, то, будь ласка, дайте мені знати, що я повинен скопіювати + вставити у цю відповідь.
guettli

1
@ miken32 спасибі Оскільки найновіша відповідь якимось чином вирішує проблему, з якою я хотів допомогти, я просто залишив коментар, який офіційні документи для 4.0 відмовляють використовувати CELERY_TASK_ALWAYS_EAGERдля тестування одиниць.
krassowski

33

Залежить від того, що саме ви хочете пройти тестування.

  • Тестуйте код завдання безпосередньо. Не дзвоніть "task.delay (...)", а просто зателефонуйте "task (...)" з ваших тестових одиниць.
  • Використовуйте CELERY_ALWAYS_EAGER . Це призведе до негайного виклику ваших завдань у момент, коли ви говорите "task.delay (...)", тому ви можете протестувати весь шлях (але не будь-яку асинхронну поведінку).

24

unittest

import unittest

from myproject.myapp import celeryapp

class TestMyCeleryWorker(unittest.TestCase):

  def setUp(self):
      celeryapp.conf.update(CELERY_ALWAYS_EAGER=True)

py.test світильники

# conftest.py
from myproject.myapp import celeryapp

@pytest.fixture(scope='module')
def celery_app(request):
    celeryapp.conf.update(CELERY_ALWAYS_EAGER=True)
    return celeryapp

# test_tasks.py
def test_some_task(celery_app):
    ...

Додаток: зробіть send_task повагою з нетерпінням

from celery import current_app

def send_task(name, args=(), kwargs={}, **opts):
    # https://github.com/celery/celery/issues/581
    task = current_app.tasks[name]
    return task.apply(args, kwargs, **opts)

current_app.send_task = send_task

22

Для тих, хто знаходиться на селері 4, це:

@override_settings(CELERY_TASK_ALWAYS_EAGER=True)

Оскільки назви параметрів були змінені та потребують оновлення, якщо ви вирішили оновити, див

https://docs.celeryproject.org/en/latest/history/whatsnew-4.0.html?highlight=what%20is%20new#lowercase-setting-names


11
Згідно з офіційними документами , використання "task_always_eager" (раніше "CELERY_ALWAYS_EAGER") не підходить для тестування одиниць. Натомість вони пропонують кілька інших чудових способів перевірити ваш додаток Селера.
krassowski

4
Я просто додам, що причина, чому ви не хочете завзяті завдання у своїх одиничних тестах, полягає в тому, що тоді ви не тестуєте, наприклад, серіалізацію параметрів, яка відбудеться, коли ви використовуєте код у виробництві.
чорт

15

Станом на Celery 3.0 , один із способів встановити CELERY_ALWAYS_EAGERу Django :

from django.test import TestCase, override_settings

from .foo import foo_celery_task

class MyTest(TestCase):

    @override_settings(CELERY_ALWAYS_EAGER=True)
    def test_foo(self):
        self.assertTrue(foo_celery_task.delay())

7

Оскільки Celery v4.0 , світильники py.test передбачені для запуску працівника селери лише для тестування, а після завершення роботи відключаються:

def test_myfunc_is_executed(celery_session_worker):
    # celery_session_worker: <Worker: gen93553@gnpill.local (running)>
    assert myfunc.delay().wait(3)

Серед інших світильників, описаних на http://docs.celeryproject.org/en/latest/userguide/testing.html#py-test , ви можете змінити параметри за замовчуванням для селери, переглянувши celery_configкріплення таким чином:

@pytest.fixture(scope='session')
def celery_config():
    return {
        'accept_content': ['json', 'pickle'],
        'result_serializer': 'pickle',
    }

За замовчуванням тестовий працівник використовує брокер в пам'яті та резервний результат. Не потрібно використовувати локальний Redis або RabbitMQ, якщо не тестувати конкретні функції.


Шановний доносець, чи хотіли б ви поділитися, чому це погана відповідь? Щиро дякую
alanjds

2
Для мене не вийшло, тестовий набір просто висить. Не могли б ви надати ще якийсь контекст? (Я все ще не голосував;)).
подвійність_

У моєму випадку мені довелося чітко встановити кріплення celey_config для використання брокера пам'яті та кешу + бекенду пам'яті
sanzoghenzo

5

посилання з використанням пістету.

def test_add(celery_worker):
    mytask.delay()

якщо ви використовуєте колбу, встановіть конфігурацію програми

    CELERY_BROKER_URL = 'memory://'
    CELERY_RESULT_BACKEND = 'cache+memory://'

і в conftest.py

@pytest.fixture
def app():
    yield app   # Your actual Flask application

@pytest.fixture
def celery_app(app):
    from celery.contrib.testing import tasks   # need it
    yield celery_app    # Your actual Flask-Celery application

2

У моєму випадку (і я припускаю багато інших), все, що я хотів, - це перевірити внутрішню логіку завдання за допомогою pytest.

TL; DR; в кінцевому підсумку знущався над усім ( ВАРІАНТ 2 )


Приклад використання :

proj/tasks.py

@shared_task(bind=True)
def add_task(self, a, b):
    return a+b;

tests/test_tasks.py

from proj import add_task

def test_add():
    assert add_task(1, 2) == 3, '1 + 2 should equal 3'

але, оскільки shared_taskдекоратор робить багато внутрішньої логіки селери, це насправді не тестування одиниці.

Отже, для мене було 2 варіанти:

ВАРІАНТ 1: Окрема внутрішня логіка

proj/tasks_logic.py

def internal_add(a, b):
    return a + b;

proj/tasks.py

from .tasks_logic import internal_add

@shared_task(bind=True)
def add_task(self, a, b):
    return internal_add(a, b);

Це виглядає дуже дивно, і окрім того, що робить його менш читабельним, потрібно вручну витягувати та передавати атрибути, які є частиною запиту, наприклад, task_idу випадку, якщо вам це потрібно, що робить логіку менш чистою.

ВАРІАНТ 2: знущається з
глузування з внутрішньої частини селери

tests/__init__.py

# noinspection PyUnresolvedReferences
from celery import shared_task

from mock import patch


def mock_signature(**kwargs):
    return {}


def mocked_shared_task(*decorator_args, **decorator_kwargs):
    def mocked_shared_decorator(func):
        func.signature = func.si = func.s = mock_signature
        return func

    return mocked_shared_decorator

patch('celery.shared_task', mocked_shared_task).start()

що дозволяє мені знущатися над об’єктом запиту (знову ж таки, якщо вам потрібні речі з запиту, наприклад, ідентифікатор або лічильник повторних спроб.

tests/test_tasks.py

from proj import add_task

class MockedRequest:
    def __init__(self, id=None):
        self.id = id or 1


class MockedTask:
    def __init__(self, id=None):
        self.request = MockedRequest(id=id)


def test_add():
    mocked_task = MockedTask(id=3)
    assert add_task(mocked_task, 1, 2) == 3, '1 + 2 should equal 3'

Це рішення набагато більш ручний, але це дає мені контроль Мені потрібно на самому ділі блок тестування, не повторюючи себе, і без втрати обсягу селери.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.