Skip to content

Commit 3115154

Browse files
authored
Merge pull request #57 from kraken-tech/add-after-commit-testing-explanation
Add explanation for testing after-commit callbacks
2 parents 70a6b0c + b13d257 commit 3115154

File tree

2 files changed

+123
-3
lines changed

2 files changed

+123
-3
lines changed

docs/static/css/theme.css

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
[data-md-color-scheme="kraken"] {
22
color-scheme: light;
3-
43
--md-primary-fg-color: #180048;
5-
64
--md-accent-fg-color: #F050F8;
75
--doc-symbol-module-fg-color: #F050F8;
86
}
97

108
[data-md-color-scheme="slate"] {
119
--md-primary-fg-color: #180048;
12-
1310
--md-accent-fg-color: #F050F8;
1411
--doc-symbol-module-fg-color: #F050F8;
1512
}
13+
14+
/* External link icon */
15+
.md-content a[href^="http"]:after {
16+
content: "↗";
17+
font-size: 0.7em;
18+
opacity: 0.7;
19+
margin-left: 0.1em;
20+
vertical-align: super;
21+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Testing after-commit callbacks
2+
3+
## The Problem
4+
5+
Django's after-commit callbacks don't work properly in tests
6+
when using Django's [`atomic`][atomic].
7+
This creates a disconnect between test behaviour and production behaviour, potentially hiding bugs.
8+
9+
Consider this function that should return `A`, `B`, `C`, `D` in order:
10+
11+
```python
12+
from functools import partial
13+
from django.db import transaction
14+
15+
def build_ABCD():
16+
my_list = []
17+
with transaction.atomic():
18+
my_list.append("A")
19+
transaction.on_commit(partial(my_list.append, "C"))
20+
my_list.append("B")
21+
my_list.append("D")
22+
return my_list
23+
```
24+
25+
### In production
26+
27+
This returns `["A", "B", "C", "D"]` as expected.
28+
29+
### In tests
30+
31+
```python
32+
from django.test import TestCase
33+
34+
class TestBuildABCD(TestCase):
35+
def test_build_ABCD():
36+
built = build_ABCD()
37+
assert built == ["A", "B", "C", "D"] # This will fail!
38+
```
39+
40+
This fails because `build_ABCD()` returns `["A", "B", "D"]`,
41+
due to the fact that the after-commit callback never runs!
42+
43+
## Why this happens
44+
45+
Django's [`TestCase`][TestCase] runs each test in a transaction,
46+
which is rolled back at the end of the test to prevent test pollution.
47+
Because the test transaction never commits, Django does not run the after-commit callbacks.
48+
49+
## The Solution: `django_subatomic.db.transaction`
50+
51+
Use Subatomic's [`transaction`][django_subatomic.db.transaction] instead of Django's [`atomic`][atomic] in your application code
52+
53+
```diff
54+
from functools import partial
55+
+from django_subatomic.db import transaction as subatomic_transaction
56+
from django.db import transaction as django_transaction
57+
58+
def build_ABCD():
59+
my_list = []
60+
- with transaction.atomic():
61+
+ with subatomic_transaction():
62+
my_list.append("A")
63+
django_transaction.on_commit(partial(my_list.append, "C"))
64+
my_list.append("B")
65+
my_list.append("D")
66+
return my_list
67+
68+
```
69+
70+
Subatomic's [`transaction`][django_subatomic.db.transaction] explicitly represents a transaction,
71+
so tests can safely run after-commit callbacks when it exits.
72+
This provides realistic production behaviour without the downsides of other approaches.
73+
74+
## Alternatives considered
75+
76+
Two alternative approaches were available
77+
to mitigate the above problem of Django's after-commit callbacks not working properly,
78+
but with some caveats.
79+
80+
### ⚠️ Using `captureOnCommitCallbacks` (timing issues)
81+
82+
```python
83+
84+
from django.test import TestCase
85+
86+
class TestBuildABCD(TestCase):
87+
def test_build_ABCD(self):
88+
with self.captureOnCommitCallbacks(execute=True)
89+
built = build_ABCD() # This returns `["A", "B", "D", "C"]`
90+
assert built == ["A", "B", "C", "D"] # This will fail!
91+
```
92+
93+
[`captureOnCommitCallbacks`][captureOnCommitCallbacks]
94+
captures and runs after-commit callbacks, but executes them after the tested function completes.
95+
While the callbacks do run, the execution order differs from production, potentially masking timing-dependent bugs.
96+
97+
### ⚠️ Using transaction test cases (potentially very slow)
98+
99+
```python
100+
from django.test import TransactionTestCase
101+
102+
class TestBuildABCD(TransactionTestCase)
103+
def test_build_ABCD():
104+
built = build_ABCD() # This returns `["A", "B", "C", "D"]`
105+
assert built == ["A", "B", "C", "D"]
106+
```
107+
108+
While the callbacks do run and the order of the results are correct,
109+
this can be extremely slow in large scale applications,
110+
because Django must truncate all tables after each test instead of rolling back a transaction.
111+
112+
[atomic]: https://docs.djangoproject.com/en/stable/topics/db/transactions/#django.db.transaction.atomic
113+
[captureOnCommitCallbacks]: https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TestCase.captureOnCommitCallbacks
114+
[TestCase]: https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TestCase

0 commit comments

Comments
 (0)