Come si esegue il test unitario di un'attività con sedano?


Risposte:


61

È 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!


1
Funziona tranne sulle attività che utilizzano un HttpDispatchTask - docs.celeryproject.org/en/latest/userguide/remote-tasks.html dove devo impostare celery.conf.CELERY_ALWAYS_EAGER = True ma anche con l'impostazione celery.conf.CELERY_IMPORTS = ('celery.task.http') il test fallisce con NotRegistered: celery.task.http.HttpDispatchTask
davidmytton

Strano, sei sicuro di non avere problemi di importazione? Questo test funziona (nota che sto simulando la risposta in modo che restituisca ciò che si aspetta il sedano). Inoltre, i moduli definiti in CELERY_IMPORTS verranno importati durante l' inizializzazione dei worker , per evitare questo ti consiglio di chiamare celery.loader.import_default_modules().
FlaPer87

Ti consiglio anche di dare un'occhiata qui . Deride la richiesta http. Non so se aiuta, immagino che tu voglia testare un servizio che sia attivo e funzionante, vero?
FlaPer87

52

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.


1
Penso che questo sia obsoleto - capisco ImportError: No module named celeryconfig.
Daenyth

7
Credo che sopra presupponga che il modulo celeryconfig.pyesista nel proprio pacchetto. Vedi docs.celeryproject.org/en/latest/getting-started/… .
Kamil Sindi

1
So che è vecchio ma puoi fornire un esempio completo su come avviare attività adddalla domanda di OP all'interno di una TestCaseclasse?
Kruupös

@ MaxChrétien mi dispiace, non posso fornire un esempio completo, dato che non uso più il sedano. Puoi modificare la mia domanda, se hai abbastanza punti reputazione. Se non ne hai abbastanza, fammi sapere cosa dovrei copiare + incollare in questa risposta.
guettli

1
@ miken32 grazie. Poiché la risposta più recente in qualche modo affronta il problema per cui volevo aiutare, ho appena lasciato un commento che i documenti ufficiali per 4.0 scoraggiano l'uso CELERY_TASK_ALWAYS_EAGERper i test unitari.
krassowski

33

Dipende da cosa esattamente vuoi testare.

  • Testare direttamente il codice dell'attività. Non chiamare "task.delay (...)" ma chiama "task (...)" dai tuoi unit test.
  • Usa CELERY_ALWAYS_EAGER . Questo farà sì che le tue attività vengano chiamate immediatamente nel punto in cui dici "task.delay (...)", quindi puoi testare l'intero percorso (ma non qualsiasi comportamento asincrono).

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 infissi

# 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):
    ...

Addendum: fai in modo che send_task rispetti desideroso

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

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

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


11
Secondo i documenti ufficiali , l'uso di "task_always_eager" (in precedenza "CELERY_ALWAYS_EAGER") non è adatto per i test unitari. Invece propongono altri ottimi modi per testare la tua app Celery.
krassowski

4
Aggiungerò solo che il motivo per cui non si desiderano attività impazienti nei test unitari è perché non si sta testando, ad esempio, la serializzazione dei parametri che avverrà una volta che si utilizza il codice in produzione.
maledetto

15

Come di sedano 3.0 , un modo per impostare CELERY_ALWAYS_EAGERin 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

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_configdispositivo 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.


Caro downvoter, vorresti condividere il motivo per cui è una cattiva risposta? Sinceramente grazie.
alanjds

2
Non ha funzionato per me, la suite di test si blocca. Potresti fornire un po 'più di contesto? (Non ho ancora votato però;)).
dualità_

Nel mio caso ho dovuto impostare esplicitamente il dispositivo celey_config per utilizzare il broker di memoria e il backend cache + memoria
sanzoghenzo

5

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

2

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_taskdecoratore 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_idnel 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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.