簡體   English   中英

你如何對 Celery 任務進行單元測試?

[英]How do you unit test a Celery task?

Celery 文檔提到在 Django 中測試 Celery,但沒有說明如果您不使用 Django,如何測試 Celery 任務。 你怎么做到這一點?

可以使用任何 unittest 庫同步測試任務。 在處理 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 夾具在單獨的線程中運行工作人員:

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 服務器。

取決於你到底想測試什么。

  • 直接測試任務代碼。 不要調用“task.delay(...)”,只需從單元測試中調用“task(...)”。
  • 使用CELERY_ALWAYS_EAGER 這將導致您的任務在您說“task.delay(...)”時立即被調用,因此您可以測試整個路徑(但不能測試任何異步行為)。

單元測試

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

對於 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

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

從 Celery v4.0開始, 提供了 py.test 固定裝置來啟動一個 celery worker 來進行測試,並在完成后關閉:

def test_myfunc_is_executed(celery_session_worker):
    # celery_session_worker: <Worker: gen93553@mymachine.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。

使用 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

就我而言(我假設還有很多其他人),我想要的只是使用 pytest 測試任務的內部邏輯。

TL;博士; 最終嘲笑一切(選項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'

該解決方案更加手動,但是,它為我提供了實際單元測試所需的控制權,無需重復自己,也不會丟失 celery 范圍。

我在單元測試方法中看到很多CELERY_ALWAYS_EAGER = true作為單元測試的解決方案,但是由於版本 5.0.5 可用,因此有很多更改使得大多數舊答案被棄用,對我來說是一個耗時的廢話,因此,對於在這里搜索解決方案的每個人,請轉到文檔並閱讀新版本的有據可查的單元測試示例:

https://docs.celeryproject.org/en/stable/userguide/testing.html

對於帶有單元測試的 Eager Mode,這里引用了實際文檔的引用:

渴望模式

task_always_eager 設置啟用的渴望模式根據定義不適合單元測試。

在使用 Eager 模式進行測試時,您只是在測試工作人員中發生的情況的仿真,並且仿真與實際發生的情況之間存在許多差異。

如果您不需要運行任務的副作用,另一種選擇是模擬任務。

from unittest import mock


@mock.patch('module.module.task')
def test_name(self, mock_task): ...

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM