testing.rst 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. Fixtures
  64. --------
  65. Function scope
  66. ~~~~~~~~~~~~~~
  67. ``celery_app`` - Celery app used for testing.
  68. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  69. This fixture returns a Celery app you can use for testing.
  70. Example:
  71. .. code-block:: python
  72. def test_create_task(celery_app, celery_worker):
  73. @celery_app.task
  74. def mul(x, y):
  75. return x * y
  76. assert mul.delay(4, 4).get(timeout=10) == 16
  77. ``celery_worker`` - Embed live worker.
  78. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  79. This fixture starts a Celery worker instance that you can use
  80. for integration tests. The worker will be started in a *separate thread*
  81. and will be shutdown as soon as the test returns.
  82. Example:
  83. .. code-block:: python
  84. # Put this in your confttest.py
  85. @pytest.fixture(scope='session')
  86. def celery_config():
  87. return {
  88. 'broker_url': 'amqp://',
  89. 'result_backend': 'redis://'
  90. }
  91. def test_add(celery_worker):
  92. mytask.delay()
  93. Session scope
  94. ~~~~~~~~~~~~~
  95. ``celery_config`` - Override to setup Celery test app configuration.
  96. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  97. You can redefine this fixture to configure the test Celery app.
  98. The config returned by your fixture will then be used
  99. to configure the :func:`celery_app`, and :func:`celery_session_app` fixtures.
  100. Example:
  101. .. code-block:: python
  102. @pytest.fixture(scope='session')
  103. def celery_config():
  104. return {
  105. 'broker_url': 'amqp://',
  106. 'result_backend': 'rpc',
  107. }
  108. ``celery_enable_logging`` - Override to enable logging in embedded workers.
  109. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  110. This is a fixture you can override to enable logging in embedded workers.
  111. Example:
  112. .. code-block:: python
  113. @pytest.fixture(scope='session')
  114. def celery_enable_logging():
  115. return True
  116. ``celery_includes`` - Add additional imports for embedded workers.
  117. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  118. You can override fixture to include modules when an embedded worker starts.
  119. You can have this return a list of module names to import,
  120. which can be task modules, modules registering signals, and so on.
  121. Example:
  122. .. code-block:: python
  123. @pytest.fixture(scope='session')
  124. def celery_includes():
  125. return [
  126. 'proj.tests.tasks',
  127. 'proj.tests.celery_signal_handlers',
  128. ]
  129. ``celery_worker_pool`` - Override the pool used for embedded workers.
  130. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  131. You can override fixture to configure the execution pool used for embedded
  132. workers.
  133. Example:
  134. .. code-block:: python
  135. @pytest.fixture(scope='session')
  136. def celery_worker_pool():
  137. return 'prefork'
  138. .. warning::
  139. You cannot use the gevent/eventlet pools, that is unless your whole test
  140. suite is running with the monkeypatches enabled.
  141. ``celery_session_worker`` - Embedded worker that lives throughout the session.
  142. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  143. This fixture starts a worker that lives throughout the testing session
  144. (it won't be started/stopped for every test).
  145. Example:
  146. .. code-block:: python
  147. # Add this to your conftest.py
  148. @pytest.fixture(scope='session')
  149. def celery_config():
  150. return {
  151. 'broker_url': 'amqp://',
  152. 'result_backend': 'rpc',
  153. }
  154. # Do this in your tests.
  155. def test_add_task(celery_session_worker):
  156. assert add.delay(2, 2) == 4
  157. .. warning::
  158. It's probably a bad idea to mix session and ephemeral workers...
  159. ``celery_session_app`` - Celery app used for testing (session scope).
  160. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  161. This can be used by other session scoped fixtures when they need to refer
  162. to a Celery app instance.
  163. ``use_celery_app_trap`` - Raise exception on falling back to default app.
  164. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  165. This is a fixture you can override in your ``conftest.py``, to enable the "app trap":
  166. if something tries to access the default or current_app, an exception
  167. is raised.
  168. Example:
  169. .. code-block:: python
  170. @pytest.fixture(scope='session')
  171. def use_celery_app_trap():
  172. return True
  173. If a test wants to access the default app, you would have to mark it using
  174. the ``depends_on_current_app`` fixture:
  175. .. code-block::
  176. @pytest.mark.usefixtures('depends_on_current_app')
  177. def test_something():
  178. something()