Introduction
Celery tasks run asynchronously and need special testing approaches to verify both task logic and integration.
Key Concepts
Task Unit Test: Test task function directly without Celery.
Eager Mode: Execute tasks synchronously for testing.
Deep Dive
Testing Task Logic Directly
pythonclass TaskLogicTests(TestCase): def test_send_notification_task(self): user = User.objects.create_user('test', 'test@test.com', 'pass') # Call task function directly (not .delay()) result = send_notification(user.id, 'Test message') self.assertTrue(result['success']) self.assertEqual(Notification.objects.count(), 1)
Using task_always_eager
python# settings/test.py (Celery 5+ lowercase setting names) task_always_eager = True task_eager_propagates = True # In tests - .delay() now runs synchronously class EagerTaskTests(TestCase): def test_task_runs_immediately(self): result = process_order.delay(order_id=1) self.assertTrue(result.successful())
Mocking Task Calls
pythonfrom unittest.mock import patch class ViewWithTaskTests(TestCase): @patch('myapp.views.send_welcome_email.delay') def test_registration_queues_email(self, mock_task): response = self.client.post(reverse('register'), { 'email': 'new@test.com', 'password': 'testpass123', }) mock_task.assert_called_once() args = mock_task.call_args[0] self.assertEqual(args[0], 'new@test.com')
Testing Retries
pythonfrom celery.exceptions import Retry class RetryTests(TestCase): @patch('myapp.tasks.external_api_call') def test_task_retries_on_failure(self, mock_api): mock_api.side_effect = ConnectionError() with self.assertRaises(Retry): sync_data()
Real World Context
Celery tasks handle background work in Django applications: sending emails, processing uploads, generating reports, and syncing data with external systems. Testing them is tricky because they normally run in a separate process. The three approaches (direct call, eager mode, mocking) each serve different testing needs.
Note: Django 6.0 introduced a built-in background tasks framework (django.tasks) that provides a lighter-weight alternative to Celery for many use cases. The testing principles covered here — direct function calls, eager execution, and mocking — apply equally to Django's built-in tasks.
Common Pitfalls
- Testing with a real Celery worker: Tests should never depend on a running Celery broker. Use eager mode or direct calls.
- Forgetting to test retry logic: If your task uses
self.retry(), test that retries happen on expected exceptions. - Not testing task arguments: When views call
task.delay(user_id), verify the correct arguments are passed using mock assertions.
Best Practices
- Use ALWAYS_EAGER for integration tests: Runs tasks synchronously.
- Test task logic separately: Call function directly without .delay().
- Mock for unit tests: Verify .delay() was called correctly.
Summary
Test Celery task logic by calling functions directly. Use task_always_eager for integration tests. Mock .delay() calls when testing views that queue tasks.