La documentazione di Celery menziona il test di Celery all'interno di Django ma non spiega come testare un'attività di Celery se non si utilizza Django. Come fai a fare questo?
La documentazione di Celery menziona il test di Celery all'interno di Django ma non spiega come testare un'attività di Celery se non si utilizza Django. Come fai a fare questo?
Risposte:
È possibile testare le attività in modo sincrono utilizzando qualsiasi libreria unittest disponibile. Normalmente eseguo 2 diverse sessioni di test quando lavoro con attività di sedano. Il primo (come sto suggerendo di seguito) è completamente sincrono e dovrebbe essere quello che si assicura che l'algoritmo faccia quello che dovrebbe fare. La seconda sessione utilizza l'intero sistema (compreso il broker) e si assicura che non abbia problemi di serializzazione o altri problemi di distribuzione o comunicazione.
Così:
from celery import Celery
celery = Celery()
@celery.task
def add(x, y):
return x + y
E il tuo test:
from nose.tools import eq_
def test_add_task():
rst = add.apply(args=(4, 4)).get()
eq_(rst, 8)
Spero che aiuti!
celery.loader.import_default_modules()
.
Io uso questo:
with mock.patch('celeryconfig.CELERY_ALWAYS_EAGER', True, create=True):
...
Documenti: http://docs.celeryproject.org/en/3.1/configuration.html#celery-always-eager
CELERY_ALWAYS_EAGER ti consente di eseguire l'attività in modo sincrono e non hai bisogno di un server per sedano.
ImportError: No module named celeryconfig
.
celeryconfig.py
esista nel proprio pacchetto. Vedi docs.celeryproject.org/en/latest/getting-started/… .
add
dalla domanda di OP all'interno di una TestCase
classe?
CELERY_TASK_ALWAYS_EAGER
per i test unitari.
Dipende da cosa esattamente vuoi testare.
import unittest
from myproject.myapp import celeryapp
class TestMyCeleryWorker(unittest.TestCase):
def setUp(self):
celeryapp.conf.update(CELERY_ALWAYS_EAGER=True)
# 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):
...
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
Per quelli su Celery 4 è:
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
Poiché i nomi delle impostazioni sono stati modificati e devono essere aggiornati se si sceglie di eseguire l'aggiornamento, vedere
Come di sedano 3.0 , un modo per impostare CELERY_ALWAYS_EAGER
in 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())
A partire da Celery v4.0 , i dispositivi py.test vengono forniti per avviare un lavoratore sedano solo per il test e vengono chiusi al termine:
def test_myfunc_is_executed(celery_session_worker):
# celery_session_worker: <Worker: gen93553@gnpill.local (running)>
assert myfunc.delay().wait(3)
Tra gli altri dispositivi descritti su http://docs.celeryproject.org/en/latest/userguide/testing.html#py-test , puoi modificare le opzioni predefinite del sedano ridefinendo il celery_config
dispositivo in questo modo:
@pytest.fixture(scope='session')
def celery_config():
return {
'accept_content': ['json', 'pickle'],
'result_serializer': 'pickle',
}
Per impostazione predefinita, il lavoratore di prova utilizza un broker in memoria e il back-end dei risultati. Non è necessario utilizzare Redis o RabbitMQ locali se non si testano funzionalità specifiche.
riferimento usando pytest.
def test_add(celery_worker):
mytask.delay()
se usi flask, imposta l'app config
CELERY_BROKER_URL = 'memory://'
CELERY_RESULT_BACKEND = 'cache+memory://'
e in 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
Nel mio caso (e presumo molti altri), tutto ciò che volevo era testare la logica interna di un'attività utilizzando pytest.
TL; DR; finito per deridere tutto ( OPZIONE 2 )
Caso d'uso di esempio :
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'
ma, dal momento che il shared_task
decoratore fa molta logica interna al sedano, non è davvero un test unitario.
Quindi, per me, c'erano 2 opzioni:
OPZIONE 1: logica interna separata
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);
Questo sembra molto strano, e oltre a renderlo meno leggibile, richiede di estrarre e passare manualmente gli attributi che fanno parte della richiesta, ad esempio task_id
nel caso in cui ne hai bisogno, che rendono la logica meno pura.
OPZIONE 2: deride
deridendo gli interni del sedano
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()
che poi mi permette di deridere l'oggetto richiesta (di nuovo, nel caso in cui hai bisogno di cose dalla richiesta, come l'id o il contatore dei tentativi.
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'
Questa soluzione è molto più manuale, ma mi dà il controllo di cui ho bisogno per effettuare un test unitario , senza ripetermi e senza perdere la portata del sedano.