Skip to content

Commit 629cafd

Browse files
author
Lode Rosseel
committed
Add docs for async, thread checks, refer to docs in plugin errors
1 parent 1a09850 commit 629cafd

File tree

3 files changed

+86
-2
lines changed

3 files changed

+86
-2
lines changed

docs/database.rst

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,87 @@ select using an argument to the ``django_db`` mark::
6060
def test_spam():
6161
pass # test relying on transactions
6262

63+
64+
Async tests and database transactions
65+
-------------------------------------
66+
67+
``pytest-django`` supports async tests that use Django's async ORM APIs.
68+
This requires the `pytest-asyncio <https://github.com/pytest-dev/pytest-asyncio>`_
69+
plugin and marking your tests appropriately.
70+
71+
Requirements
72+
""""""""""""
73+
74+
- Install ``pytest-asyncio``.
75+
- Mark async tests with both ``@pytest.mark.asyncio`` and
76+
``@pytest.mark.django_db`` (or request the ``db``/``transactional_db`` fixtures).
77+
78+
Example (async ORM with transactional rollback per test)::
79+
80+
import pytest
81+
82+
@pytest.mark.asyncio
83+
@pytest.mark.django_db
84+
async def test_async_db_is_isolated():
85+
assert await Item.objects.acount() == 0
86+
await Item.objects.acreate(name="example")
87+
assert await Item.objects.acount() == 1
88+
# changes are rolled back after the test
89+
90+
.. _`async-db-behavior`:
91+
92+
Behavior of ``db`` in async tests
93+
"""""""""""""""""""""""""""""""""
94+
95+
Tests using ``db`` wrap each test in a transaction and roll that transaction back at the end
96+
(like ``django.test.TestCase``). In Django, transactions are bound to the database
97+
connection, which is unique per thread. This means that all your database changes
98+
must be made within the same thread to ensure they are rolled back before the next test.
99+
100+
Django Async ORM calls, as of writing, use the ``asgiref.sync.sync_to_async``
101+
decorator to run the ORM calls on a dedicated thread executor.
102+
103+
For async tests, pytest-django ensures the transaction
104+
setup/teardown happens via ``asgiref.sync.sync_to_async``, which means the transaction is started & run on the
105+
same thread on which async orm calls inside your test, like ``aget()`` are made. This ensures your test code
106+
can safely modify the database using the async calls, as all its queries will be rolled back after the test.
107+
108+
Tests using ``transactional_db`` flush the database between tests. This means that no matter in which thread
109+
your test modifies the database, the changes will be removed after the test. This means you can avoid thinking
110+
about sync/async database access if your test uses ``transactional_db``, at the cost of slower tests:
111+
A flush is generally slower than rolling back a transaction.
112+
113+
.. _`db-thread-safeguards`:
114+
Safeguards against database access from different threads
115+
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
116+
When using the database in a test with transaction rollback, you must ensure that
117+
database access is only done from the same thread that the test is running on.
118+
119+
To avoid your fixtures/tests making changes outside the test thread, and as a result, the transaction, pytest-django
120+
actively restricts where database connections may be opened:
121+
122+
- In async tests using ``db``: database access is only allowed from the single
123+
thread used by ``SyncToAsync``. Using sync fixtures that touch the database in
124+
an async test will raise::
125+
126+
RuntimeError: Database access is only allowed in an async context, modify your
127+
test fixtures to be async or use the transactional_db fixture.
128+
129+
Fix by converting those fixtures to async (use ``pytest_asyncio.fixture``) and
130+
using Django's async ORM methods (e.g. ``.acreate()``, ``.aget()``, ``.acount()``),
131+
or by requesting ``transactional_db`` if you must keep sync fixtures.
132+
See :ref:`async-db-behavior` for more details.
133+
134+
- In sync tests: database access is only allowed from the main thread. Attempting to use the database connection
135+
from a different thread will raise::
136+
137+
RuntimeError: Database access is only allowed in the main thread, modify your
138+
test fixtures to be sync or use the transactional_db fixture.
139+
140+
Fix this by ensuring all database transactions run in the main thread (e.g., avoiding the use of async fixtures),
141+
or use ``transactional_db`` to allow mixing.
142+
143+
63144
.. _`multi-db`:
64145

65146
Tests requiring multiple databases
@@ -524,3 +605,4 @@ Put this in ``conftest.py``::
524605
django_db_blocker.unblock()
525606
yield
526607
django_db_blocker.restore()
608+

pytest_django/fixtures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def _sync_django_db_helper(
239239
"django_db_serialized_rollback" in request.fixturenames
240240
)
241241

242-
with django_db_blocker.unblock():
242+
with django_db_blocker.unblock(sync_only=not transactional):
243243
import django.db
244244
import django.test
245245

pytest_django/plugin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,7 @@ def _blocking_wrapper(self, *args, **kwargs) -> NoReturn:
839839
raise RuntimeError(
840840
"Database access not allowed, "
841841
'use the "django_db" mark, or the '
842-
'"db" or "transactional_db" fixtures to enable it.'
842+
'"db" or "transactional_db" fixtures to enable it. '
843843
)
844844

845845
def _unblocked_async_only(self, *args, **kwargs):
@@ -853,6 +853,7 @@ def _unblocked_async_only(self, *args, **kwargs):
853853
raise RuntimeError(
854854
"Database access is only allowed in an async context, "
855855
"modify your test fixtures to be async or use the transactional_db fixture."
856+
"See https://pytest-django.readthedocs.io/en/latest/database.html#db-thread-safeguards for more information."
856857
)
857858
elif self._real_ensure_connection is not None:
858859
self._real_ensure_connection(*args, **kwargs)
@@ -863,6 +864,7 @@ def _unblocked_sync_only(self, *args, **kwargs):
863864
raise RuntimeError(
864865
"Database access is only allowed in the main thread, "
865866
"modify your test fixtures to be sync or use the transactional_db fixture."
867+
"See https://pytest-django.readthedocs.io/en/latest/database.html#db-thread-safeguards for more information."
866868
)
867869
elif self._real_ensure_connection is not None:
868870
self._real_ensure_connection(*args, **kwargs)

0 commit comments

Comments
 (0)