คุณทดสอบหน่วยขึ้นฉ่ายได้อย่างไร?


คำตอบ:


61

เป็นไปได้ที่จะทดสอบงานพร้อมกันโดยใช้ lib ที่ไม่เหมาะสมที่สุด ปกติฉันทำแบบทดสอบ 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

ฉันยังอยากจะแนะนำให้คุณลองดูที่นี่ มันล้อเลียนคำขอ 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แล้ว
Daenyth

7
ฉันเชื่อว่าข้างต้นถือว่าโมดูลceleryconfig.pyมีอยู่ในแพ็คเกจ ดูdocs.celeryproject.org/en/latest/getting-started/… .
Kamil Sindi

1
ฉันรู้ว่ามันเก่า แต่ช่วยยกตัวอย่างวิธีเปิดงานaddจากคำถามของ OP ภายในTestCaseชั้นเรียนได้ไหม
Kruupös

@ MaxChrétienขออภัยฉันไม่สามารถให้ตัวอย่างทั้งหมดได้เนื่องจากฉันไม่ได้ใช้คื่นช่ายอีกต่อไป คุณสามารถแก้ไขคำถามของฉันได้หากคุณมีคะแนนชื่อเสียงเพียงพอ หากคุณมีไม่เพียงพอโปรดแจ้งให้เราทราบว่าฉันควรคัดลอก + วางอะไรในคำตอบนี้
guettli

1
@ miken32 ขอบคุณ. เนื่องจากคำตอบล่าสุดสามารถแก้ไขปัญหาที่ฉันต้องการช่วยได้ฉันจึงแสดงความคิดเห็นว่าเอกสารอย่างเป็นทางการสำหรับ 4.0 ไม่สนับสนุนการใช้CELERY_TASK_ALWAYS_EAGERสำหรับการทดสอบหน่วย
krassowski

33

ขึ้นอยู่กับสิ่งที่คุณต้องการทดสอบ

  • ทดสอบรหัสงานโดยตรง อย่าเรียก "task.delay (... )" เพียงแค่เรียก "task (... )" จากการทดสอบหน่วยของคุณ
  • ใช้CELERY_ALWAYS_EAGER สิ่งนี้จะทำให้งานของคุณถูกเรียกทันทีเมื่อคุณพูดว่า "task.delay (... )" คุณจึงสามารถทดสอบเส้นทางทั้งหมดได้ (แต่ไม่ใช่พฤติกรรมอะซิงโครนัสใด ๆ )

24

Unittest

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

24

สำหรับผู้ที่ขึ้นฉ่าย 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_EAGERในDjangoคือ:

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

ตั้งแต่คื่นฉ่ายv4.0 , ติดตั้ง py.test จะจัดให้มีการเริ่มต้นของผู้ปฏิบัติงานคื่นฉ่ายเพียงสำหรับการทดสอบและมีการปิดตัวลงเมื่อทำ:

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

2
ไม่ได้ผลสำหรับฉันชุดทดสอบก็หยุดทำงาน คุณช่วยระบุบริบทเพิ่มเติมได้ไหม (ฉันยังไม่ได้ลงคะแนน;))
duality_

ในกรณีของฉันฉันต้องตั้งค่าการติดตั้ง celey_config อย่างชัดเจนเพื่อใช้โบรกเกอร์หน่วยความจำและแคช + แบ็กเอนด์หน่วยความจำ
sanzoghenzo

6

การอ้างอิง โดยใช้ 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()

ซึ่งจะทำให้ฉันสามารถเยาะเย้ยออบเจ็กต์คำขอได้ (อีกครั้งในกรณีที่คุณต้องการสิ่งต่างๆจากคำขอเช่นรหัสหรือตัวนับการลองใหม่

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.