Database is not properly rolled back after async tests
JakeUrban opened this issue ยท 8 comments
When using pytest-django's pytest.mark.django_db marker in conjunction with pytest.mark.asyncio, any writes to the database are not rolled back when the test completes and affect subsequent tests.
To test, I created a fresh django app, installed pytest-django and pytest-asyncio, created a simple model, and wrote two tests:
models.py:
from django.db import models
class MyModel(models.Model):
my_field = models.TextField(null=True, blank=True)test_app.py:
import pytest
from asgiref.sync import sync_to_async
from app.models import MyModel
@pytest.mark.django_db
@pytest.mark.asyncio
async def test_save_model_to_db():
await sync_to_async(MyModel.objects.create)()
@pytest.mark.django_db
def test_check_if_model_present():
assert MyModel.objects.count() == 0Running pytest resulted in the first test succeeding and the second failing:
@pytest.mark.django_db
def test_check_if_model_present():
> assert MyModel.objects.count() == 0
E assert 1 == 0At first I thought it might be due to how Django runs synchronous code from an asynchronous context. Maybe it was creating the instance in another transaction that pytest wasn't rolling back.
So, I updated the test code to set DJANGO_ALLOW_ASYNC_UNSAFE to "true" so I could remove the sync_to_async() wrapper around the create() call, ensuring the instance would be included created in the same thread and within pytest's transaction.
Unfortunately, the test failed for the same reason. This could be an issue with pytest-asyncio or compatibility with pytest-django.
Looks like there is an issue filed on pytest-django too: pytest-dev/pytest-django#580
This also could be a django-specific, non-pytest related problem: https://code.djangoproject.com/ticket/32409
Is there any update on this? I'm definitely seeing data persist between tests after converting them to async with pytest fixtures. E.g. if one test changes a user's password, the next test starts with that changed password.
It seems that switching to pytest.mark.django_db(transaction=True) fixes the leak, but that slows down the tests A LOT (like 3x), so it's not a good solution for large test suites.
Has anyone found a better way to roll back the data between tests?
@pcraciunoiu I am using this "fix" in my codebase: django/channels#1091 (comment)
Even if the thread is about Django channels, that was good enough to fix this issue for me (i am not using channels at all, just Django with pytest-asyncio and pytest-django).
My current understanding of the issue is that database transactions are not properly rolled by when using asgiref.sync.sync_to_async. This is also the general issue discussed in the Django channels thread linked by @scastlara.
sync_to_async executes the wrapped function in a thread pool, in order to prevent blocking the event loop. Under the hood, it seems to retrieve the running event loop and to submit the wrapped callable via loop.run_in_executor.
Pytest-asyncio neither sets nor modifies the default event loop executor. It's currently unclear to me how pytest-asyncio can address this issue.
By default, pytest-asyncio provides a new event loop for each test case. The code provided by the OP of this (pytest-asyncio) does not overwrite the event_loop fixture. Hence, the tests use the default function-scoped event loop. As such, I don't see how sync_to_async is relevant for the test.
If anyone can provide a more complete reproducer and/or explain how the issue is related to pytest-asyncio, I'm happy to look into it.
I just encountered this issue, but seems to be resolved by explicitly marking the test cases with a transaction: @pytest.mark.django_db(transaction=True)
Worked for me, at least.
@saxelsen that does work, but as noted above, it slows down tests A LOT. Not really a good solution if you have lots of tests.
It seems the fix from the django-channels thread broke with asgiref v3.8, so I'm pinning that to continue using it, for now.
Has anyone found a better solution? Without this patch + without marking them transactional, the tests just hang for me.