'How to test a Django on_commit hook without clearing the database?
The on_commit function has been added to Django 1.9 to be able to trigger an action (e.g. a Celery task) after the current transaction has committed.
They mention later in the docs that one should use TransactionTestCase
to test features that rely on that function. However, unlike TestCase
(which uses transactions and rolls them back), TransactionTestCase
empties the whole database after each test.
Unfortunately, I have data migrations that preload some useful data inside the database, which means that subsequent tests do not work anymore after the first test clears the database.
I ended up resorting to a dirty trick by mocking on_commit
:
with mock.patch.object(django.db.transaction, 'on_commit', lambda t: t()):
test_something()
Is there a better way?
Solution 1:[1]
Starting with version 3.2 Django has a build-in way to test the on_comit hook. Example:
from django.core import mail
from django.test import TestCase
class ContactTests(TestCase):
def test_post(self):
with self.captureOnCommitCallbacks(execute=True) as callbacks:
response = self.client.post(
'/contact/',
{'message': 'I like your site'},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(callbacks), 1)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'Contact Form')
self.assertEqual(mail.outbox[0].body, 'I like your site')
Here is the official documentation: https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TestCase.captureOnCommitCallbacks
Solution 2:[2]
Just keep using TestCase and fake commit forcing executing of posponed actions in run_and_clear_commit_hooks. Check this article:
https://medium.com/gitux/speed-up-django-transaction-hooks-tests-6de4a558ef96
Solution 3:[3]
Adam Johnson wrote this, and I think the code referenced here does the trick:
https://adamj.eu/tech/2020/05/20/the-fast-way-to-test-django-transaction-on-commit-callbacks/
@contextmanager
def captureOnCommitCallbacks(cls, *, using=DEFAULT_DB_ALIAS, execute=False):
"""Context manager to capture transaction.on_commit() callbacks."""
callbacks = []
start_count = len(connections[using].run_on_commit)
try:
yield callbacks
finally:
run_on_commit = connections[using].run_on_commit[start_count:]
callbacks[:] = [func for sids, func in run_on_commit]
if execute:
for callback in callbacks:
callback()
usage:
class ContactTests(TestCase):
def test_post(self):
with self.captureOnCommitCallbacks(execute=True) as callbacks:
response = self.client.post(
'/contact/',
{'message': 'I like your site'},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(callbacks), 1)
Solution 4:[4]
I have two possibilities in mind:
- as this section says
post_migrate
emitted after flush, so you can perform preloading some useful data - You can subclass
TransactionTestCase
and implement your_fixture_teardown
(you can see that flush is called there in the very end of method).
I'd probably stick with first one if your migration isn't too expensive and with second one if it is.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Tim Tisdall |
Solution 2 | Juan Madurga |
Solution 3 | Craig Wallace |
Solution 4 | MyNameIsCaleb |