# Flask實作_消息隊列_Testing with Celery ###### tags: `flask` `celery` `celery 4.4` ## History 20191119_依據4.4版本說明翻譯 ## 目錄 [TOC] ## 參考 [官方連結_Testing with Celery](http://docs.celeryproject.org/en/master/userguide/testing.html) ## Testing with Celery ### Tasks and unit tests 要在單元測試中測試任務行為,首選的方法是模擬。 #### Eager mode: 根據定義,由[`task_always_eager`](http://docs.celeryproject.org/en/master/userguide/configuration.html#std:setting-task_always_eager)設置啟用的eager mode並不適用於單元測試。 當使用eager model測試的時候,你只是測試Worker中發生的事情的模擬,而且模擬與實際發生的事情之間有在許多差異。 Celery任務非常類似web view,因為它只定義如何在被稱為任務的上下文中執行操作。 這意味著最佳任務僅處理序列化、訊息標頭、重試等內容,而實際邏輯則在其它地方實現。 假設我們有一個任務像這個: ```python from .models import Product @app.task(bind=True) def send_order(self, product_pk, quantity, price): price = Decimal(price) # json serializes this to string. # models are passed by id, not serialized. product = Product.objects.get(product_pk) try: product.order(quantity, price) except OperationalError as exc: raise self.retry(exc=exc) ``` 注意:任務的[綁定](http://docs.celeryproject.org/en/latest/userguide/tasks.html#bound-tasks)意味著任務的第一個參數將總是任務實例(`self`)。這意味著你 確實將`self`做為第一個參數,並且可以使用`Task`類的方法與屬性。 你可以使用模擬來寫為這個任務寫單元測試,就像下面範例: ```python from pytest import raises from celery.exceptions import Retry # for python 2: use mock.patch from `pip install mock`. from unittest.mock import patch from proj.models import Product from proj.tasks import send_order class test_send_order: @patch('proj.tasks.Product.order') # < patching Product in module above def test_success(self, product_order): product = Product.objects.create( name='Foo', ) send_order(product.pk, 3, Decimal(30.3)) product_order.assert_called_with(3, Decimal(30.3)) @patch('proj.tasks.Product.order') @patch('proj.tasks.send_order.retry') def test_failure(self, send_order_retry, product_order): product = Product.objects.create( name='Foo', ) # Set a side effect on the patched methods # so that they raise the errors we want. send_order_retry.side_effect = Retry() product_order.side_effect = OperationalError() with raises(Retry): send_order(product.pk, 3, Decimal(30.6)) ``` ### Py.test *New in version 4.0.* Celery也是[pytest](https://pypi.org/project/pytest/)插件,它添加了可以在集成(或單元)測試套件中使用的設置。 #### Marks ##### celery - Set test app configuration. celery的標記讓你可以覆寫用於單個測試案例的配置: ```python @pytest.mark.celery(result_backend='redis://') def test_something(): ... ``` 或是在一個類別中的所有測試案例: ```python @pytest.mark.celery(result_backend='redis://') class test_something: def test_one(self): ... def test_two(self): ... ``` #### Fixtures ##### Function scope celery_app **- Celery app used for testing**. 這個設置會回傳可用於測試的Celery應用程式。 範例: ```python def test_create_task(celery_app, celery_worker): @celery_app.task def mul(x, y): return x * y assert mul.delay(4, 4).get(timeout=10) == 16 ``` celery_worker **- Embed live worker**. 這個設置啟動一個Celery worker實例,你可以用它來集成測試。這個worker會在一個隔離的[線程](http://terms.naer.edu.tw/detail/2430706/)中被啟動,並在測試回傳之後立即關閉。 範例: ```python @pytest.fixture(scope='session') def celery_config(): return { 'broker_url': 'amqp://', 'result_backend': 'redis://' } def test_add(celery_worker): mytask.delay() # If you wish to override some setting in one test cases # only - you can use the ``celery`` mark: @pytest.mark.celery(result_backend='rpc') def test_other(celery_worker): ... ``` ##### Session scope celery_config **- Override to setup Celery test app configuration**. 你可以重新定義這個設置來配置測試Celery應用程式。 然後,透過你的設置回傳的配置會用來配置**celery_app()**,與**celery_session_app()**。 範例: ```python def celery_config(): return { 'broker_url': 'amqp://', 'result_backend': 'rpc', } ``` celery_parameters **- Override to setup Celery test app parameters**. 你可以重新定義這個設置來改變測試Celery應用程式的__init__參數。與`celery_config()`相比,這些設置會在實例化Celery的時候直接傳遞。 然後,透過你的設置回傳的配置會用來配置**celery_app()**,與**celery_session_app()**。 範例: ```python @pytest.fixture(scope='session') def celery_parameters(): return { 'task_cls': my.package.MyCustomTaskClass, 'strict_typing': False, } ``` celery_worker_parameters **- Override to setup Celery worker parameters**. 你可以重新定義這個設置來改變測試Celery worker的__init__參數。這些設置會在初始化的時候直接傳遞給[WorkController](http://docs.celeryproject.org/en/master/reference/celery.worker.html#celery.worker.WorkController) 然後,透過你的設置回傳的配置會用來配置**celery_worker()**,與**celery_session_worker()**。 範例: ```python @pytest.fixture(scope='session') def celery_worker_parameters(): return { 'queues': ('high-prio', 'low-prio'), 'exclude_queues': ('celery'), } ``` celery_enable_logging **- Override to enable logging in embedded workers**. 你可以在嵌入式worker啟動的時候覆寫設置來包括模組。 你可以讓它回傳要導入的模組名稱清單,清單可以是任務模組,註冊信號的模組,等等。 範例: ```python @pytest.fixture(scope='session') def celery_includes(): return [ 'proj.tests.tasks', 'proj.tests.celery_signal_handlers', ] ``` celery_worker_pool **- Override the pool used for embedded workers**. 你可以覆寫設置來配置用於嵌入式workers的執行池。 範例: ```python @pytest.fixture(scope='session') def celery_worker_pool(): return 'prefork' ``` :::warning **Warning:** 你不能使用gevent/eventlet pools,除非你整個測試套件都在啟用monkeypatches的情況下執行。 ::: celery_session_worker **- Embedded worker that lives throughout the session**. 這個設置啟動一個在整個測試過程中都存活的worker(不會在每次測試中被啟動/停止)。 ```python # Add this to your conftest.py @pytest.fixture(scope='session') def celery_config(): return { 'broker_url': 'amqp://', 'result_backend': 'rpc', } # Do this in your tests. def test_add_task(celery_session_worker): assert add.delay(2, 2) == 4 ``` :::warning **Warning:** 將session與臨時的worker混在一起不是一個好主意… ::: celery_session_app **- Celery app used for testing (session scope)**. 當其它會話範圍的設置需要引用Celery應用程式實例的時候,可以使用此功能。 use_celery_app_trap **- Raise exception on falling back to default app**. 你可以在你的`conftest.py`覆寫這個設置,以啟用"app trap":如果有東西試著訪問預設或當前應用程式(`current_app`),則會拋出異常。 範例: ```python @pytest.fixture(scope='session') def use_celery_app_trap(): return True ``` 如果有一個測試想訪問預設的應用程式,你應該使用depends_on_current_app設置來標記它: ```python @pytest.mark.usefixtures('depends_on_current_app') def test_something(): something() ```