testing.rst 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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(send_order_retry, product_order):
  50. product = Product.objects.create(
  51. name='Foo',
  52. )
  53. # set a side effect on the patched method
  54. # so that it raises the error we want.
  55. product_order.side_effect = OperationalError()
  56. with raises(Retry):
  57. send_order(product.pk, 3, Decimal(30.6))
  58. Py.test
  59. =======
  60. .. versionadded:: 4.0
  61. Celery is also a :pypi:`pytest` plugin that adds fixtures that you can
  62. use in your integration (or unit) test suites.
  63. Marks
  64. -----
  65. ``celery`` - Set test app configuration.
  66. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  67. The ``celery`` mark enables you to override the configuration
  68. used for a single test case:
  69. .. code-block:: python
  70. @pytest.mark.celery(result_backend='redis://')
  71. def test_something():
  72. ...
  73. or for all the test cases in a class:
  74. .. code-block:: python
  75. @pytest.mark.celery(result_backend='redis://')
  76. class test_something:
  77. def test_one(self):
  78. ...
  79. def test_two(self):
  80. ...
  81. Fixtures
  82. --------
  83. Function scope
  84. ^^^^^^^^^^^^^^
  85. ``celery_app`` - Celery app used for testing.
  86. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  87. This fixture returns a Celery app you can use for testing.
  88. Example:
  89. .. code-block:: python
  90. def test_create_task(celery_app, celery_worker):
  91. @celery_app.task
  92. def mul(x, y):
  93. return x * y
  94. assert mul.delay(4, 4).get(timeout=10) == 16
  95. ``celery_worker`` - Embed live worker.
  96. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  97. This fixture starts a Celery worker instance that you can use
  98. for integration tests. The worker will be started in a *separate thread*
  99. and will be shutdown as soon as the test returns.
  100. Example:
  101. .. code-block:: python
  102. # Put this in your confttest.py
  103. @pytest.fixture(scope='session')
  104. def celery_config():
  105. return {
  106. 'broker_url': 'amqp://',
  107. 'result_backend': 'redis://'
  108. }
  109. def test_add(celery_worker):
  110. mytask.delay()
  111. # If you wish to override some setting in one test cases
  112. # only - you can use the ``celery`` mark:
  113. @pytest.mark.celery(result_backend='rpc')
  114. def test_other(celery_worker):
  115. ...
  116. Session scope
  117. ^^^^^^^^^^^^^
  118. ``celery_config`` - Override to setup Celery test app configuration.
  119. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  120. You can redefine this fixture to configure the test Celery app.
  121. The config returned by your fixture will then be used
  122. to configure the :func:`celery_app`, and :func:`celery_session_app` fixtures.
  123. Example:
  124. .. code-block:: python
  125. @pytest.fixture(scope='session')
  126. def celery_config():
  127. return {
  128. 'broker_url': 'amqp://',
  129. 'result_backend': 'rpc',
  130. }
  131. ``celery_parameters`` - Override to setup Celery test app parameters.
  132. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  133. You can redefine this fixture to change the ``__init__`` parameters of test
  134. Celery app. In contrast to :func:`celery_config`, these are directly passed to
  135. when instantiating :class:`~celery.Celery`.
  136. The config returned by your fixture will then be used
  137. to configure the :func:`celery_app`, and :func:`celery_session_app` fixtures.
  138. Example:
  139. .. code-block:: python
  140. @pytest.fixture(scope='session')
  141. def celery_parameters():
  142. return {
  143. 'task_cls': my.package.MyCustomTaskClass,
  144. 'strict_typing': False,
  145. }
  146. ``celery_worker_parameters`` - Override to setup Celery worker parameters.
  147. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  148. You can redefine this fixture to change the ``__init__`` parameters of test
  149. Celery workers. These are directly passed to
  150. :class:`~celery.worker.WorkController` when it is instantiated.
  151. The config returned by your fixture will then be used
  152. to configure the :func:`celery_worker`, and :func:`celery_session_worker`
  153. fixtures.
  154. Example:
  155. .. code-block:: python
  156. @pytest.fixture(scope='session')
  157. def celery_worker_parameters():
  158. return {
  159. 'queues': ('high-prio', 'low-prio'),
  160. 'exclude_queues': ('celery'),
  161. }
  162. ``celery_enable_logging`` - Override to enable logging in embedded workers.
  163. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  164. This is a fixture you can override to enable logging in embedded workers.
  165. Example:
  166. .. code-block:: python
  167. @pytest.fixture(scope='session')
  168. def celery_enable_logging():
  169. return True
  170. ``celery_includes`` - Add additional imports for embedded workers.
  171. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  172. You can override fixture to include modules when an embedded worker starts.
  173. You can have this return a list of module names to import,
  174. which can be task modules, modules registering signals, and so on.
  175. Example:
  176. .. code-block:: python
  177. @pytest.fixture(scope='session')
  178. def celery_includes():
  179. return [
  180. 'proj.tests.tasks',
  181. 'proj.tests.celery_signal_handlers',
  182. ]
  183. ``celery_worker_pool`` - Override the pool used for embedded workers.
  184. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  185. You can override fixture to configure the execution pool used for embedded
  186. workers.
  187. Example:
  188. .. code-block:: python
  189. @pytest.fixture(scope='session')
  190. def celery_worker_pool():
  191. return 'prefork'
  192. .. warning::
  193. You cannot use the gevent/eventlet pools, that is unless your whole test
  194. suite is running with the monkeypatches enabled.
  195. ``celery_session_worker`` - Embedded worker that lives throughout the session.
  196. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  197. This fixture starts a worker that lives throughout the testing session
  198. (it won't be started/stopped for every test).
  199. Example:
  200. .. code-block:: python
  201. # Add this to your conftest.py
  202. @pytest.fixture(scope='session')
  203. def celery_config():
  204. return {
  205. 'broker_url': 'amqp://',
  206. 'result_backend': 'rpc',
  207. }
  208. # Do this in your tests.
  209. def test_add_task(celery_session_worker):
  210. assert add.delay(2, 2) == 4
  211. .. warning::
  212. It's probably a bad idea to mix session and ephemeral workers...
  213. ``celery_session_app`` - Celery app used for testing (session scope).
  214. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  215. This can be used by other session scoped fixtures when they need to refer
  216. to a Celery app instance.
  217. ``use_celery_app_trap`` - Raise exception on falling back to default app.
  218. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  219. This is a fixture you can override in your ``conftest.py``, to enable the "app trap":
  220. if something tries to access the default or current_app, an exception
  221. is raised.
  222. Example:
  223. .. code-block:: python
  224. @pytest.fixture(scope='session')
  225. def use_celery_app_trap():
  226. return True
  227. If a test wants to access the default app, you would have to mark it using
  228. the ``depends_on_current_app`` fixture:
  229. .. code-block:: python
  230. @pytest.mark.usefixtures('depends_on_current_app')
  231. def test_something():
  232. something()