testing.rst 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. .. _testing:
  2. ================================================================
  3. Testing with Celery
  4. ================================================================
  5. Tasks and unit tests
  6. ====================
  7. To test task behavior in unit tests the preferred method is mocking.
  8. .. admonition:: Eager mode
  9. The eager mode enabled by the :setting:`task_always_eager` setting
  10. is by definition not suitable for unit tests.
  11. When testing with eager mode you are only testing an emulation
  12. of what happens in a worker, and there are many discrepancies
  13. between the emulation and what happens in reality.
  14. A Celery task is much like a web view, in that it should only
  15. define how to perform the action in the context of being called as a task.
  16. This means optimally tasks only handle things like serialization, message headers,
  17. retries, and so on, with the actual logic implemented elsewhere.
  18. Say we had a task like this:
  19. .. code-block:: python
  20. from .models import Product
  21. @app.task(bind=True)
  22. def send_order(self, product_pk, quantity, price):
  23. price = Decimal(price) # json serializes this to string.
  24. # models are passed by id, not serialized.
  25. product = Product.objects.get(product_pk)
  26. try:
  27. product.order(quantity, price)
  28. except OperationalError as exc:
  29. raise self.retry(exc=exc)
  30. You could write unit tests for this task, using mocking like
  31. in this example:
  32. .. code-block:: python
  33. from pytest import raises
  34. from celery.exceptions import Retry
  35. # for python 2: use mock.patch from `pip install mock`.
  36. from unittest.mock import patch
  37. from proj.models import Product
  38. from proj.tasks import send_order
  39. class test_send_order:
  40. @patch('proj.tasks.Product.order') # < patching Product in module above
  41. def test_success(self, product_order):
  42. product = Product.objects.create(
  43. name='Foo',
  44. )
  45. send_order(product.pk, 3, Decimal(30.3))
  46. product_order.assert_called_with(3, Decimal(30.3))
  47. @patch('proj.tasks.Product.order')
  48. @patch('proj.tasks.send_order.retry')
  49. def test_failure(self, send_order_retry, product_order):
  50. product = Product.objects.create(
  51. name='Foo',
  52. )
  53. # Set a side effect on the patched methods
  54. # so that they raise the errors we want.
  55. send_order_retry.side_effect = Retry()
  56. product_order.side_effect = OperationalError()
  57. with raises(Retry):
  58. send_order(product.pk, 3, Decimal(30.6))
  59. Py.test
  60. =======
  61. .. versionadded:: 4.0
  62. Celery is also a :pypi:`pytest` plugin that adds fixtures that you can
  63. use in your integration (or unit) test suites.
  64. Marks
  65. -----
  66. ``celery`` - Set test app configuration.
  67. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  68. The ``celery`` mark enables you to override the configuration
  69. used for a single test case:
  70. .. code-block:: python
  71. @pytest.mark.celery(result_backend='redis://')
  72. def test_something():
  73. ...
  74. or for all the test cases in a class:
  75. .. code-block:: python
  76. @pytest.mark.celery(result_backend='redis://')
  77. class test_something:
  78. def test_one(self):
  79. ...
  80. def test_two(self):
  81. ...
  82. Fixtures
  83. --------
  84. Function scope
  85. ^^^^^^^^^^^^^^
  86. ``celery_app`` - Celery app used for testing.
  87. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  88. This fixture returns a Celery app you can use for testing.
  89. Example:
  90. .. code-block:: python
  91. def test_create_task(celery_app, celery_worker):
  92. @celery_app.task
  93. def mul(x, y):
  94. return x * y
  95. assert mul.delay(4, 4).get(timeout=10) == 16
  96. ``celery_worker`` - Embed live worker.
  97. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  98. This fixture starts a Celery worker instance that you can use
  99. for integration tests. The worker will be started in a *separate thread*
  100. and will be shutdown as soon as the test returns.
  101. Example:
  102. .. code-block:: python
  103. # Put this in your conftest.py
  104. @pytest.fixture(scope='session')
  105. def celery_config():
  106. return {
  107. 'broker_url': 'amqp://',
  108. 'result_backend': 'redis://'
  109. }
  110. def test_add(celery_worker):
  111. mytask.delay()
  112. # If you wish to override some setting in one test cases
  113. # only - you can use the ``celery`` mark:
  114. @pytest.mark.celery(result_backend='rpc')
  115. def test_other(celery_worker):
  116. ...
  117. Session scope
  118. ^^^^^^^^^^^^^
  119. ``celery_config`` - Override to setup Celery test app configuration.
  120. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  121. You can redefine this fixture to configure the test Celery app.
  122. The config returned by your fixture will then be used
  123. to configure the :func:`celery_app`, and :func:`celery_session_app` fixtures.
  124. Example:
  125. .. code-block:: python
  126. @pytest.fixture(scope='session')
  127. def celery_config():
  128. return {
  129. 'broker_url': 'amqp://',
  130. 'result_backend': 'rpc',
  131. }
  132. ``celery_parameters`` - Override to setup Celery test app parameters.
  133. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  134. You can redefine this fixture to change the ``__init__`` parameters of test
  135. Celery app. In contrast to :func:`celery_config`, these are directly passed to
  136. when instantiating :class:`~celery.Celery`.
  137. The config returned by your fixture will then be used
  138. to configure the :func:`celery_app`, and :func:`celery_session_app` fixtures.
  139. Example:
  140. .. code-block:: python
  141. @pytest.fixture(scope='session')
  142. def celery_parameters():
  143. return {
  144. 'task_cls': my.package.MyCustomTaskClass,
  145. 'strict_typing': False,
  146. }
  147. ``celery_worker_parameters`` - Override to setup Celery worker parameters.
  148. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  149. You can redefine this fixture to change the ``__init__`` parameters of test
  150. Celery workers. These are directly passed to
  151. :class:`~celery.worker.WorkController` when it is instantiated.
  152. The config returned by your fixture will then be used
  153. to configure the :func:`celery_worker`, and :func:`celery_session_worker`
  154. fixtures.
  155. Example:
  156. .. code-block:: python
  157. @pytest.fixture(scope='session')
  158. def celery_worker_parameters():
  159. return {
  160. 'queues': ('high-prio', 'low-prio'),
  161. 'exclude_queues': ('celery'),
  162. }
  163. ``celery_enable_logging`` - Override to enable logging in embedded workers.
  164. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  165. This is a fixture you can override to enable logging in embedded workers.
  166. Example:
  167. .. code-block:: python
  168. @pytest.fixture(scope='session')
  169. def celery_enable_logging():
  170. return True
  171. ``celery_includes`` - Add additional imports for embedded workers.
  172. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  173. You can override fixture to include modules when an embedded worker starts.
  174. You can have this return a list of module names to import,
  175. which can be task modules, modules registering signals, and so on.
  176. Example:
  177. .. code-block:: python
  178. @pytest.fixture(scope='session')
  179. def celery_includes():
  180. return [
  181. 'proj.tests.tasks',
  182. 'proj.tests.celery_signal_handlers',
  183. ]
  184. ``celery_worker_pool`` - Override the pool used for embedded workers.
  185. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  186. You can override fixture to configure the execution pool used for embedded
  187. workers.
  188. Example:
  189. .. code-block:: python
  190. @pytest.fixture(scope='session')
  191. def celery_worker_pool():
  192. return 'prefork'
  193. .. warning::
  194. You cannot use the gevent/eventlet pools, that is unless your whole test
  195. suite is running with the monkeypatches enabled.
  196. ``celery_session_worker`` - Embedded worker that lives throughout the session.
  197. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  198. This fixture starts a worker that lives throughout the testing session
  199. (it won't be started/stopped for every test).
  200. Example:
  201. .. code-block:: python
  202. # Add this to your conftest.py
  203. @pytest.fixture(scope='session')
  204. def celery_config():
  205. return {
  206. 'broker_url': 'amqp://',
  207. 'result_backend': 'rpc',
  208. }
  209. # Do this in your tests.
  210. def test_add_task(celery_session_worker):
  211. assert add.delay(2, 2) == 4
  212. .. warning::
  213. It's probably a bad idea to mix session and ephemeral workers...
  214. ``celery_session_app`` - Celery app used for testing (session scope).
  215. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  216. This can be used by other session scoped fixtures when they need to refer
  217. to a Celery app instance.
  218. ``use_celery_app_trap`` - Raise exception on falling back to default app.
  219. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  220. This is a fixture you can override in your ``conftest.py``, to enable the "app trap":
  221. if something tries to access the default or current_app, an exception
  222. is raised.
  223. Example:
  224. .. code-block:: python
  225. @pytest.fixture(scope='session')
  226. def use_celery_app_trap():
  227. return True
  228. If a test wants to access the default app, you would have to mark it using
  229. the ``depends_on_current_app`` fixture:
  230. .. code-block:: python
  231. @pytest.mark.usefixtures('depends_on_current_app')
  232. def test_something():
  233. something()