Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions constance/test/unittest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from functools import wraps

from asgiref.sync import iscoroutinefunction
from asgiref.sync import sync_to_async
from django import VERSION as DJANGO_VERSION
from django.test import SimpleTestCase
from django.test.utils import override_settings
Expand Down Expand Up @@ -27,10 +29,22 @@ def __call__(self, test_func):
raise Exception("Only subclasses of Django SimpleTestCase can be decorated with override_config")
return self.modify_test_case(test_func)

@wraps(test_func)
def inner(*args, **kwargs):
with self:
return test_func(*args, **kwargs)
if iscoroutinefunction(test_func):

@wraps(test_func)
async def inner(*args, **kwargs):
try:
await sync_to_async(self.enable, thread_sensitive=True)()
return await test_func(*args, **kwargs)
finally:
await sync_to_async(self.disable, thread_sensitive=True)()

else:

@wraps(test_func)
def inner(*args, **kwargs):
with self:
return test_func(*args, **kwargs)

return inner

Expand Down
5 changes: 5 additions & 0 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ method level and also as a
self.assertEqual(config.YOUR_FAVOURITE_COLOR, "Blue?")


The method decorator also supports ``async def`` tests. The overridden values
remain active until the awaited test completes, including when it raises an
exception.


Pytest usage
~~~~~~~~~~~~

Expand Down
54 changes: 54 additions & 0 deletions tests/test_test_overrides.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import asyncio
from unittest.mock import patch

from asgiref.sync import async_to_sync
from asgiref.sync import iscoroutinefunction
from django.test import TestCase
from django.test import TransactionTestCase

from constance import config
from constance.test import override_config
Expand Down Expand Up @@ -35,3 +41,51 @@ class OverrideConfigClassDecoratorTestCase(TestCase):
def test_override_config_on_class_changes_config_value(self):
"""Assert that the class decorator changes config.BOOL_VALUE."""
self.assertFalse(config.BOOL_VALUE)


class OverrideConfigAsyncDecoratorTestCase(TransactionTestCase):
@override_config(BOOL_VALUE=False)
async def test_override_config_on_async_method(self):
self.assertFalse(await config.BOOL_VALUE)
await asyncio.sleep(0)
self.assertFalse(await config.BOOL_VALUE)

def test_async_override_lasts_until_await_completes(self):
@override_config(BOOL_VALUE=False)
async def decorated():
self.assertFalse(await config.BOOL_VALUE)
await asyncio.sleep(0)
self.assertFalse(await config.BOOL_VALUE)
return "completed"

self.assertTrue(iscoroutinefunction(decorated))
self.assertEqual(async_to_sync(decorated)(), "completed")
self.assertTrue(config.BOOL_VALUE)

def test_async_override_restores_after_exception(self):
@override_config(BOOL_VALUE=False)
async def decorated():
self.assertFalse(await config.BOOL_VALUE)
await asyncio.sleep(0)
raise ValueError("test failure")

with self.assertRaisesMessage(ValueError, "test failure"):
async_to_sync(decorated)()
self.assertTrue(config.BOOL_VALUE)

def test_async_override_with_database_backend(self):
from constance import settings
from constance.base import Config

with patch.object(settings, "BACKEND", "constance.backends.database.DatabaseBackend"):
database_config = Config()
with patch("constance.test.unittest.config", database_config):

@override_config(BOOL_VALUE=False)
async def decorated():
self.assertFalse(await database_config.BOOL_VALUE)
await asyncio.sleep(0)
self.assertFalse(await database_config.BOOL_VALUE)

async_to_sync(decorated)()
self.assertTrue(database_config.BOOL_VALUE)
Loading