From 8954cec5a18c9ff57e02711ca4f4cc5f472d3fc7 Mon Sep 17 00:00:00 2001 From: Ben Thorner Date: Thu, 8 Apr 2021 10:01:51 +0100 Subject: [PATCH] Add tests for celery task superclass This requires upgrading freezegun, as time.monotonic wasn't frozen by v1.0. Note that we need to explicitly specify the base class for the task in the test, the reason for which is quite subtle: - Normally, by using the 'notify_api' fixture, the base class is set to NotifyTask automatically by running app.create_app [1]. - However, when run alongside other tests, the imports of files with other celery tasks cause the base class to be instantiated and cached as the default Celery one. This means none of our tests actually use our custom superclass when testing tasks. Because we can't run 'apply_async' directly (since this would require an actual Celery broker), we need to manually push/pop the request Context that's normally done as part of sending a task. Note also that we use a UUID as the name for a task, since these are global. We want to avoid the task polluting other tests in future, as well as make it clear the task is being reused. [1]: https://github.com/alphagov/notifications-api/blob/dea5828d0e708bc69916a98e2bff989a56112204/app/__init__.py#L113 --- requirements_for_test.txt | 2 +- tests/app/celery/test_celery.py | 76 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/app/celery/test_celery.py diff --git a/requirements_for_test.txt b/requirements_for_test.txt index 3b9beafba..c9862ea0a 100644 --- a/requirements_for_test.txt +++ b/requirements_for_test.txt @@ -8,7 +8,7 @@ pytest-env==0.6.2 pytest-mock==3.3.1 pytest-cov==2.10.1 pytest-xdist==2.1.0 -freezegun==1.0.0 +freezegun==1.1.0 requests-mock==1.8.0 # used for creating manifest file locally jinja2-cli[yaml]==0.7.0 diff --git a/tests/app/celery/test_celery.py b/tests/app/celery/test_celery.py new file mode 100644 index 000000000..bea1588e6 --- /dev/null +++ b/tests/app/celery/test_celery.py @@ -0,0 +1,76 @@ +import uuid + +import pytest +from freezegun import freeze_time + +from app import notify_celery + + +@pytest.fixture(scope='session') +def celery_task(): + @notify_celery.task(name=uuid.uuid4(), base=notify_celery.task_cls) + def test_task(delivery_info=None): pass + return test_task + + +@pytest.fixture +def async_task(celery_task): + celery_task.push_request(delivery_info={'routing_key': 'test-queue'}) + yield celery_task + celery_task.pop_request() + + +def test_success_should_log_and_call_statsd(mocker, notify_api, async_task): + statsd = mocker.patch.object(notify_api.statsd_client, 'timing') + logger = mocker.patch.object(notify_api.logger, 'info') + + with freeze_time() as frozen: + async_task() + frozen.tick(5) + + async_task.on_success( + retval=None, task_id=1234, args=[], kwargs={} + ) + + statsd.assert_called_once_with(f'celery.test-queue.{async_task.name}.success', 5.0) + logger.assert_called_once_with(f'Celery task {async_task.name} (queue: test-queue) took 5.0000') + + +def test_success_queue_when_applied_synchronously(mocker, notify_api, celery_task): + statsd = mocker.patch.object(notify_api.statsd_client, 'timing') + logger = mocker.patch.object(notify_api.logger, 'info') + + with freeze_time() as frozen: + celery_task() + frozen.tick(5) + + celery_task.on_success( + retval=None, task_id=1234, args=[], kwargs={} + ) + + statsd.assert_called_once_with(f'celery.none.{celery_task.name}.success', 5.0) + logger.assert_called_once_with(f'Celery task {celery_task.name} (queue: none) took 5.0000') + + +def test_failure_should_log_and_call_statsd(mocker, notify_api, async_task): + statsd = mocker.patch.object(notify_api.statsd_client, 'incr') + logger = mocker.patch.object(notify_api.logger, 'exception') + + async_task.on_failure( + exc=Exception, task_id=1234, args=[], kwargs={}, einfo=None + ) + + statsd.assert_called_once_with(f'celery.test-queue.{async_task.name}.failure') + logger.assert_called_once_with(f'Celery task {async_task.name} (queue: test-queue) failed') + + +def test_failure_queue_when_applied_synchronously(mocker, notify_api, celery_task): + statsd = mocker.patch.object(notify_api.statsd_client, 'incr') + logger = mocker.patch.object(notify_api.logger, 'exception') + + celery_task.on_failure( + exc=Exception, task_id=1234, args=[], kwargs={}, einfo=None + ) + + statsd.assert_called_once_with(f'celery.none.{celery_task.name}.failure') + logger.assert_called_once_with(f'Celery task {celery_task.name} (queue: none) failed')