您如何对Celery任务进行单元测试?


Answers:


61

可以使用任何unittest库同步测试任务。在处理芹菜任务时,我通常会进行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 = True设置为True,即使同时还要设置celery.conf.CELERY_IMPORTS = ('celery.task.http')测试失败,但未注册:celery.task.http.HttpDispatchTask
davidmytton 2012年

很奇怪,您确定没有任何导入问题吗?该测试有效(请注意,我在伪造响应,因此它返回了芹菜的期望值)。另外,在CELERY_IMPORTS中定义的模块将在worker 初始化期间导入,为避免这种情况,建议您调用celery.loader.import_default_modules()
FlaPer87

我也建议你在这里看看。它模拟了http请求。Dunno知道是否有帮助,我想您想测试已启动并正在运行的服务,不是吗?
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
丹妮丝2015年

7
我相信以上假设该模块celeryconfig.py位于自己的包装中。参见docs.celeryproject.org/en/latest/getting-started/…
卡米尔·辛迪

1
我知道它已经很老了,但是您能否提供一个完整的示例,说明如何addTestCase班级中的OP问题中启动任务?
Kruupös

@MaxChrétien对不起,我无法提供完整的例子,因为我不再使用芹菜了。如果您有足够的信誉点,则可以编辑我的问题。如果您没有足够的空间,请告诉我该复制并粘贴的内容。
guettli

1
@ miken32谢谢。当最新的答案以某种方式解决了我想帮助的问题时,我只留下了一条评论,即4.0版的官方文档不鼓励将其CELERY_TASK_ALWAYS_EAGER用于单元测试。
krassowski

33

取决于您要测试的内容。

  • 直接测试任务代码。不要调用“ task.delay(...)”,而只是在单元测试中调用“ task(...)”。
  • 使用CELERY_ALWAYS_EAGER。这将导致您的任务在您说“ task.delay(...)”时立即被调用,因此您可以测试整个路径(但不能测试任何异步行为)。

24

单元测试

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

对于Celery 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”)的使用不适合单元测试。相反,他们提出了其他一些很棒的方法来对您的Celery应用程序进行单元测试。
krassowski

4
我只是要补充一下,为什么您不希望在单元测试中执行急切的任务的原因是因为您当时没有进行测试,例如在生产中使用代码后将发生的参数序列化。
该死的

15

Celery 3.0开始CELERY_ALWAYS_EAGERDjango中进行设置的一种方法是:

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固定装置来启动celery工人只是为了进行测试,并在完成后将其关闭:

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以下方式重新定义灯具来更改celery的默认选项:

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

默认情况下,测试人员使用内存中的代理和结果后端。如果不测试特定功能,则无需使用本地Redis或RabbitMQ。


亲爱的唐纳德,您想分享一下为什么这个答案不好吗?衷心的感谢。
alanjds

2
对我不起作用,测试套件挂起。您能否提供更多背景信息?(不过我还没有投票;)。
duality_

以我为例,我必须显式设置celey_config固定装置以使用内存代理和缓存+内存后端
sanzoghenzo

5

使用pytest 参考

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

然后,它允许我模拟请求对象(同样,如果您需要请求中的内容,例如ID或重试计数器)。

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'

该解决方案更加手动,但是,它为我提供了实际进行单元测试所需的控制,而无需重复自己,也不会丢失芹菜范围。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.