Celeryタスクをどのように単体テストしますか?


回答:


61

unittest libを使用して、同期的にタスクをテストすることができます。私は通常、セロリタスクを操作するときに2つの異なるテストセッションを行います。最初のものは(私が以下を提案しているように)完全に同期的であり、アルゴリズムが実行すべきことを確実に実行するものでなければなりません。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にでさえも設定celery.conf.CELERY_IMPORTSと私が持っています= ( 'celery.task.http')テストはNotRegisteredで失敗します:celery.task.http.HttpDispatchTask
davidmytton

奇妙なことに、インポートの問題が発生していませんか?このテストは機能します(セロリが期待するものを返すように、私は応答を偽装していることに注意してください)。また、CELERY_IMPORTSで定義されたモジュールは、ワーカーの初期化中にインポートされますcelery.loader.import_default_modules()。これを回避するために、呼び出すことをお勧めします。
FlaPer87 2012

こちらもご覧になることをお勧めします。それはhttp要求を模倣します。Dunnoが役に立ったかどうかはわかりますが、稼働中のサービスをテストしたいと思いませんか。
FlaPer87 2012

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を使用すると、タスクを同期的に実行でき、Celeryサーバーは必要ありません。


1
これは時代遅れだと思いますImportError: No module named celeryconfig
Daenyth、2015年

7
上記では、モジュールceleryconfig.pyがパッケージ内にあると想定しています。docs.celeryproject.org/en/latest/getting-started/…を参照してください。
カミルシンディ2016年

1
私はそれが古いことを知っていaddますが、TestCaseクラス内でOPの質問からタスクを起動する方法の完全な例を提供できますか?
クルーポス2017

@MaxChrétien申し訳ありませんが、セロリはもう使用しないため、完全な例を提供することはできません。十分な評判ポイントがあれば、私の質問を編集できます。十分でない場合は、この回答に何をコピーして貼り付ければよいかをお知らせください。
guettli 2017

1
@ miken32ありがとう。最新の回答がどうにかして私が手助けしたかった問題に取り組んでいるので、4.0の公式ドキュメントはCELERY_TASK_ALWAYS_EAGER単体テストでの使用を推奨しないというコメントを残しました。
Krassowski 2017

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

セロリ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
単体テストで熱心なタスクを望まない理由は、たとえば、本番環境でコードを使用したときに発生するパラメーターのシリアル化などのテストを行わないためです。
DAMD

15

とおりセロリ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フィクスチャを次のように再定義することにより、セロリのデフォルトオプションを変更できます。

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

デフォルトでは、テストワーカーはインメモリブローカーと結果バックエンドを使用します。特定の機能をテストしない場合は、ローカルのRedisまたはRabbitMQを使用する必要はありません。


親愛なるダウンボーター、なぜこれが悪い答えなのか共有したいですか?心から感謝します。
alanjds 2018

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'

このソリューションははるかに手作業ですが、自分自身を繰り返したり、セロリのスコープを失うことなく、実際に単体テストを実行するために必要なコントロールを提供します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.