Celery 文档提到在 Django 中测试 Celery,但没有解释如果您不使用 Django 如何测试 Celery 任务。你怎么做到这一点?
可以使用任何单元测试库同步测试任务。在处理 celery 任务时,我通常会进行 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)
这是我七年前的答案的更新:
您可以通过
pytest fixture
在单独的线程中运行工作人员:
https://docs.celeryq.dev/en/v5.2.6/userguide/testing.html#celery-worker-embed-live-worker
根据文档,您不应使用
"always_eager"
(请参阅上述链接的页面顶部)。
旧答案:
我用这个:
with mock.patch('celeryconfig.CELERY_ALWAYS_EAGER', True, create=True):
...
文档:https://docs.celeryq.dev/en/3.1/configuration.html#celery-always-eager
CELERY_ALWAYS_EAGER
让您同步运行任务,并且不需要 celery 服务器。
取决于您到底想测试什么。
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
对于 Celery 4 的用户来说:
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
由于设置名称已更改,如果您选择升级则需要更新,请参阅
从 Celery 3.0 开始,在
Django中设置
CELERY_ALWAYS_EAGER
的一种方法是:
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())
参考 使用 pytest。
def test_add(celery_worker):
mytask.delay()
如果您使用flask,请设置应用程序配置
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
自 Celery v4.0 起,提供 py.test 固定装置来启动 celery 工作程序以进行测试,并在完成后关闭:
def test_myfunc_is_executed(celery_session_worker):
# celery_session_worker: <Worker: [email protected] (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。
就我而言(我假设还有许多其他人),我想要的只是使用 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
装饰器做了很多 celery 内部逻辑,所以它并不是真正的单元测试。
所以,对我来说,有两个选择:
选项 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'
这个解决方案更加手动,但是,它为我提供了实际 unit 测试所需的控制,无需重复自己,也不会丢失 celery 范围。
我在单元测试方法中看到很多
CELERY_ALWAYS_EAGER = true
作为单元测试的解决方案,但自从版本5.0.5可用以来,有很多变化,这使得大多数旧答案被弃用,对我来说这是一个耗时的废话,因此对于这里搜索解决方案的每个人,请转到文档并阅读新版本的详细记录的单元测试示例:
https://docs.celeryproject.org/en/stable/userguide/testing.html
对于带有单元测试的 Eager 模式,这里引用了实际文档:
渴望模式
task_always_eager 设置启用的 eager 模式是 定义不适合单元测试。
当使用 eager 模式进行测试时,您只是在测试什么的模拟 发生在工人身上,并且两者之间存在许多差异 仿真和现实中发生的情况。
如果您不需要运行任务的副作用,另一种选择是模拟任务。
from unittest import mock
@mock.patch('module.module.task')
def test_name(self, mock_task): ...