Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django: Override Setting used in AppConfig Ready Function

We are trying to write an automated test for the behavior of the AppConfig.ready function, which we are using as an initialization hook to run code when the Django app has loaded. Our ready method implementation uses a Django setting that we need to override in our test, and naturally we're trying to use the override_settings decorator to achieve this.

There is a snag however - when the test runs, at the point the ready function is executed, the setting override hasn't kicked in (it is still using the original value from settings.py). Is there a way that we can still override the setting in a way where the override will apply when the ready function is called?

Some code to demonstrate this behavior:

settings.py

MY_SETTING = 'original value'

dummy_app/__init__.py

default_app_config = 'dummy_app.apps.DummyAppConfig'

dummy_app/apps.py

from django.apps import AppConfig
from django.conf import settings


class DummyAppConfig(AppConfig):
    name = 'dummy_app'

    def ready(self):
        print('settings.MY_SETTING in app config ready function: {0}'.format(settings.MY_SETTING))

dummy_app/tests.py

from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings


@override_settings(MY_SETTING='overridden value')
@override_settings(INSTALLED_APPS=('dummy_app',))
class AppConfigTests(TestCase):

    def test_to_see_where_overridden_settings_value_is_available(self):
        print('settings.MY_SETTING in test function: '.format(settings.MY_SETTING))
        self.fail('Trigger test output')

Output

======================================================================
FAIL: test_to_see_where_overridden_settings_value_is_available (dummy_app.tests.AppConfigTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/labminds/venv/labos/src/dom-base/dummy_app/tests.py", line 12, in test_to_see_where_overridden_settings_value_is_available
    self.fail('Trigger test output')
AssertionError: Trigger test output
-------------------- >> begin captured stdout << ---------------------
settings.MY_SETTING in app config ready function: original value
settings.MY_SETTING in test function: overridden value

--------------------- >> end captured stdout << ----------------------

It is important to note that we only want to override this setting for the tests that are asserting the behavior of ready, which is why we aren't considering changing the setting in settings.py, or using a separate version of this file used just for running our automated tests.

One option already considered - we could simply initialize the AppConfig class in our test, call ready and test the behavior that way (at which point the setting would be overridden by the decorator). However, we would prefer to run this as an integration test, and rely on the natural behavior of Django to call the function for us - this is key functionality for us and we want to make sure the test fails if Django's initialization behavior changes.

like image 402
robjohncox Avatar asked Jun 30 '15 20:06

robjohncox


2 Answers

You appear to have hit a documented limitation of ready in Django (scroll down to the warning). You can see the discussion in the ticket that prompted the edit. The ticket specifically refers to database interactions, but the same limitation would apply to any effort to test the ready function -- i.e. that production (not test) settings are used during ready.

Based on the ticket, "don't use ready" sounds like the official answer, but I don't find that attitude useful unless they direct me to a functionally equivalent place to run this kind of initialization code. ready seems to be the most official place to run once on startup.

Rather than (re)calling ready, I suggest having ready call a second method. Import and use that second method in your tests cases. Not only will your tests be cleaner, but it isolates the test case from any other ready logic like attaching signals. There's also a context manager that can be used to simplify the test:

@override_settings(SOME_SETTING='some-data')
def test(self):
    ...

or

def test(self):
    with override_settings(SOME_SETTING='some-data'):
        ...

P.S. We work around several possible issues in ready by checking the migration status of the system:

def ready(self):
    # imports have to be delayed for ready
    from django.db.migrations.executor import MigrationExecutor
    from django.conf import settings
    from django.db import connections, DEFAULT_DB_ALIAS

    executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
    plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
    if plan:
        # not healthy (possibly setup for a migration)
        return

    ...

Perhaps something similar could be done to prevent execution during tests. Somehow the system knows to (eventually) switch to test settings. I assume you could skip execution under the same conditions.

like image 180
claytond Avatar answered Oct 30 '22 22:10

claytond


Some ideas (different effort required and automated assurance):

  • Don't integration test, and rely on reading the releas notes/commits before upgrading the Django version and / or rely on single manual testing
  • Assuming a test - stage deploy - prod deploy pipeline, unit test the special cases in isolation and add an integration check as a deployment smoke test (e.g.: by exposing this settings value through a management command or internal only url endpoint) - only verify that for staging it has the value it should be for staging. Slightly delayed feedback compared to unit tests
  • test it through a test framework outside of Django's own - i.e.: write the unittests (or py.tests) and inside those tests bootstrap django in each test (though you need a way to import & manipulate the settings)
  • use a combination of overriding settings via the OS's environment (we've used envdir a'la 12 factor app) and a management command that would do the test(s) - e.g.: MY_SETTING='overridden value' INSTALLED_APPS='dummy_app' EXPECTED_OUTCOME='whatever' python manage.py ensure_app_config_initialized_as_expected
  • looking at Django's own app init tests apps.clear_cache() and with override_settings(INSTALLED_APPS=['test_app']): config = apps.get_app_config('test_app') assert config.... could work, though I've never tried it
like image 45
zsepi Avatar answered Oct 30 '22 21:10

zsepi