web-dev-qa-db-fra.com

Comment testez-vous une tâche de céleri?

La documentation sur le céleri mentionne avoir testé le céleri dans Django mais n'explique pas comment tester une tâche de céleri si vous n'utilisez pas Django. Comment est-ce que tu fais ça?

94
DavidM

Il est possible de tester des tâches de manière synchrone en utilisant n'importe quelle bibliothèque unittest. Je fais normalement 2 sessions de test différentes lorsque je travaille avec des tâches de céleri. Le premier (comme je le suggère ci-dessous) est complètement synchrone et devrait être celui qui garantit que l'algorithme fait ce qu'il devrait faire. La deuxième session utilise l’ensemble du système (y compris le courtier) et évite les problèmes de sérialisation ou tout autre problème de communication ou de distribution.

Alors:

from celery import Celery

celery = Celery()

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

Et votre test:

from nose.tools import eq_

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

J'espère que ça t'as aidé!

52
FlaPer87

J'utilise ceci:

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

Docs: http://docs.celeryproject.org/en/3.1/configuration.html#celery-always-eager

CELERY_ALWAYS_EAGER vous permet d'exécuter votre tâche de manière synchrone et vous n'avez pas besoin d'un serveur de céleri.

47
guettli

Cela dépend de ce que vous voulez exactement tester.

  • Testez le code de tâche directement. N'appelez pas "task.delay (...)", appelez simplement "task (...)" depuis vos tests unitaires.
  • Utilisez CELERY_ALWAYS_EAGER . Vos tâches seront immédiatement appelées au moment où vous dites "task.delay (...)", de sorte que vous pouvez tester le chemin d'accès complet (mais pas le moindre comportement asynchrone).
26
slacy

test de l'unité

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

Addendum: faites en sorte que send_task respecte

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
ksindi

Pour ceux sur Celery 4 c'est:

@override_settings(CELERY_TASK_ALWAYS_EAGER=True)

Les noms des paramètres ayant été modifiés et devant être mis à jour si vous choisissez de procéder à une mise à niveau, voir

http://docs.celeryproject.org/en/latest/whatsnew-4.0.html#lowercase-setting-names

19
okrutny

À partir de Celery 3., une façon de définir CELERY_ALWAYS_EAGER dans Django est:

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())
13
Aaron Lelevier

Depuis céleri v4.0 , les montages py.test sont fournis pour démarrer un céleri-rave juste pour le test et sont arrêtés lorsque vous avez terminé:

def test_myfunc_is_executed(celery_session_worker):
    # celery_session_worker: <Worker: [email protected] (running)>
    assert myfunc.delay().wait(3)

Parmi les autres appareils décrits sur http://docs.celeryproject.org/en/latest/userguide/testing.html#py-test , vous pouvez modifier les options de céleri par défaut en redéfinissant le celery_config luminaire de cette façon:

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

Par défaut, l'agent de test utilise un courtier en mémoire et un backend de résultat. Pas besoin d'utiliser un Redis ou un RabbitMQ local si vous ne testez pas des fonctionnalités spécifiques.

3
alanjds

Dans mon cas (et j'en suppose beaucoup d'autres), tout ce que je voulais, c'était de tester la logique interne d'une tâche à l'aide de pytest.

TL; DR; finit par se moquer de tout (OPTION 2)


Exemple d'utilisation:

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'

mais depuis shared_task Le décorateur fait beaucoup de logique interne au céleri, ce n'est pas vraiment un test unitaire.

Donc, pour moi, il y avait 2 options:

OPTION 1: Logique interne séparée

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);

Cela semble très étrange et, mis à part le rendre moins lisible, il faut extraire et transmettre manuellement les attributs faisant partie de la requête, par exemple le task_id au cas où vous en auriez besoin, ce qui rendrait la logique moins pure.

OPTION 2: moque
se moquer des internes de céleri

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()

ce qui me permet ensuite de simuler l'objet de requête (encore une fois, au cas où vous auriez besoin d'éléments de la requête, comme l'identifiant ou le compteur de tentatives).

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'

Cette solution est beaucoup plus manuelle, mais elle me donne le contrôle dont j'ai besoin pour réellement nité tester, sans me répéter, et sans perdre la portée du céleri.

2
Daniel Dubovski