How to overwrite django app settings in unittests? - python

Django allows overwriting settings, when running tests through SimpleTestCase.settings() (https://docs.djangoproject.com/en/1.8/topics/testing/tools/#django.test.SimpleTestCase.settings). That works fine when I try to overwrite one of Django's settings.
The app I want to run tests for carries its own settings in an app-specific settings.py with the following structure to allow overwriting the app-specific settings in the project wide settings.py:
from django.conf import settings
APP_SETTING1 = getattr(settings, 'APP_SETTING1', 'foo')
The following dummy code in a test shows the problem:
from django.test import TestCase
from django.conf import settings as django_settings
from my_app import settings
class MyTestCase(TestCase):
def test_something(self):
with self.settings(APP_SETTING1='bar'):
print(django_settings.APP_SETTING1) # bar
print(settings.APP_SETTING1) # foo
Why isn't that working and how can I make it work?

you can use django-zero-settings, it lets you define your default settings, and will update them with user settings provided, it also works fine in tests settings overrides, alongside that it auto import string, lets you define removed settings and pre-check user settings too.
as in your case, you can define app settings like this:
from zero_settings import ZeroSettings
app_settings = ZeroSettings(
key="APP",
defaults={
"SETTING1": "foo"
},
)
then in your tests you can use it like:
from django.test import TestCase
from django.conf import settings as django_settings
from my_app import app_settings
class MyTestCase(TestCase):
def test_something(self):
with self.settings(APP={"SETTING1": "bar"}):
print(django_settings.APP["SETTING1"]) # bar
print(app_settings.SETTING1) # bar
self.assertEqual(django_settings.APP["SETTING1"], app_settings.SETTING1)
because that ZeroSettings uses cache and will cache user settings on the first attempt to get key, you may need to set it off before getting keys, to make sure you get the latest updates:
app_settings = ZeroSettings(
key="APP",
defaults={
"SETTING1": "foo"
},
use_cache=False,
)
or clear the cache manually:
class MyTestCase(TestCase):
def test_something(self):
print(app_settings.SETTING1) # foo
with self.settings(APP={"SETTING1": "bar"}):
app_settings._clear_cache()
print(django_settings.APP["SETTING1"]) # bar
print(app_settings.SETTING1) # bar
self.assertEqual(django_settings.APP["SETTING1"], app_settings.SETTING1)

Ideally you should mock settings using unittest library incorporated into Django. Alternatively, you can do this, but it is better to patch those:
https://docs.djangoproject.com/en/2.1/topics/settings/#custom-default-settings

Actually, there are good practices for handling multiple settings files. The basic rules are:
1) Do not import from settings.py file directly:
# BAD - avoid this
from app.settings import SOME_ENV
Because if in the end, someone will try to use custom settings some parts of the old settings will be applied somewhere.
# GOOD
from django.conf import settings
print(settings.SOME_ENV)
In this case, you could be sure that actual settings were used.
2) Split up settings files for different environments, usually, it accomplished via creating several files:
settings/base.py
DEBUG = False
AWS_STORAGE_PREFIX = 'local'
...
settings/dev.py
from settings.base import *
DEBUG = True
AWS_STORAGE_PREFIX = 'dev'
DATABASE = {# your settings for dev env}
...
It is also quite usual to create a separated file for test runner because it gives you more power of controlling your test env and so on:
python manage.py test --settings=app.settings.testrunner
Test instance has bound settings' context manager, so you can override any variables that you need for testing:
class MyTestCase(TestCase):
def test_something(self):
# Old settings
with self.settings(SOME_ENV_VAR='overridden'):
# Overridden settings
pass
...

You can do it directly in apps using a certain setting
settings.py:
TEST_ENVIRONMENT_SETTING = True # Test setting for my app
apps.py:
from django.conf import settings
class MyAppConfig(AppConfig):
def ready(self):
if getattr(settings, 'TEST_ENVIRONMENT_SETTING', False):
# Do stuff or not do stuff
Or you can directly set this setting in your TestCase using
setattr(settings, 'TEST_ENVIRONMENT_SETTING', True)

I'm struggling with the same problem, and I think the only solution is to change approach and use only django.conf.settings directly.
The reason is that if you use a custom settings, your custom settings is taken too early from settings, before override_settings or TestCase.settings is called.

Related

How to use pytest `request.config` inside the django.test.TestCase class setUp function

I am using pytest in my django project. I used pytest.config previously to get conftest.py values into the setUp function but it is deprecated since version 4.1. In the docs they suggest to use request fixture to get configs but I couldn't find a proper way get configs inside to the TestCase class using request fixture. Following code sample is the working code which uses the pytest.config. Any way to get configs without getting PytestDeprecationWarning will be helpful.
import pytest
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings
#override_settings(USE_SUBDOMAIN=False, PRODUCTION_DOMAIN='example.com')
class TestSample(TestCase):
fixtures = ['test_data']
def setUp(self):
url_base = '{scheme}://{domain}/sample'.format(
scheme=pytest.config.option.url_scheme,
domain=settings.PRODUCTION_DOMAIN,
)

Best practice for defining default app settings for custom django module?

Is there a defined best practice for defining custom settings that come with a django app.
What I have at the moment is a separate file called app_settings.py that looks like this:
from django.conf import settings
# MY_SETTING_1 is required and will brake the application if not defined
try:
MY_SETTING_1 = str(getattr(settings, 'MY_SETTING_1'))
except AttributeError:
raise ImproperlyConfigured ('MY_SETTING_1 not defined in settings.py.')
# MY_SETTING_2 is not required and has a default value
MY_SETTING_2 = getattr(settings, 'MY_SETTING_2', ['default_value'])
Then I am importing this in my views.py, and using it like this:
from my_app import app_settings
print (app_settings.MY_SETTING_1)
print (app_settings.MY_SETTING_2)
This works ok, but I am not quite happy with the design. I am assuming that I can somehow use the class defined in apps.py, but I am not sure how.
Is there a best (or better) practice for this?
You could try https://pypi.org/project/typed_app_settings/.
Example:
# my_app/app_settings.py
from typed_app_settings import UndefinedValue, typed_app_settings_dict
#typed_app_settings_dict("MY_APP")
class Settings:
MY_SETTING_1: str = UndefinedValue()
MY_SETTING_2: str = "default_value"
settings = Settings()
Then in the view you would just use it like this.
from my_app.app_settings import settings
print(app_settings.MY_SETTING_1)
print(app_settings.MY_SETTING_2)

Django library: define custom settings [duplicate]

The Django documentation mentions that you can add your own settings to django.conf.settings. So if my project's settings.py defines
APPLES = 1
I can access that with settings.APPLES in my apps in that project.
But if my settings.py doesn't define that value, accessing settings.APPLES obviously won't work. Is there some way to define a default value for APPLES that is used if there is no explicit setting in settings.py?
I'd like best to define the default value in the module/package that requires the setting.
In my apps, I have a seperate settings.py file. In that file I have a get() function that does a look up in the projects settings.py file and if not found returns the default value.
from django.conf import settings
def get(key, default):
return getattr(settings, key, default)
APPLES = get('APPLES', 1)
Then where I need to access APPLES I have:
from myapp import settings as myapp_settings
myapp_settings.APPLES
This allows an override in the projects settings.py, getattr will check there first and return the value if the attribute is found or use the default defined in your apps settings file.
How about just:
getattr(app_settings, 'SOME_SETTING', 'default value')
Here are two solutions. For both you can set settings.py files in your applications and fill them with default values.
Configure default value for a single application
Use from MYAPP import settings instead of from django.conf import settings in your code.
Edit YOURAPP/__init__.py:
from django.conf import settings as user_settings
from . import settings as default_settings
class AppSettings:
def __getattr__(self, name):
# If the setting you want is filled by the user, let's use it.
if hasattr(user_settings, name):
return getattr(user_settings, name)
# If the setting you want has a default value, let's use it.
if hasattr(default_settings, name):
return getattr(default_settings, name)
raise AttributeError("'Settings' object has no attribute '%s'" % name)
settings = AppSettings()
Configure default values for a whole project
Use from MYPROJECT import settings instead of from django.conf import settings in your code.
Edit MYPROJECT/MYPROJECT/__init__.py
import os, sys, importlib
from . import settings as user_settings
def get_local_apps():
"""Returns the locally installed apps names"""
apps = []
for app in user_settings.INSTALLED_APPS:
path = os.path.join(user_settings.BASE_DIR, app)
if os.path.exists(path) and app != __name__:
apps.append(sys.modules[app])
return apps
class AppSettings:
SETTINGS_MODULE = 'settings'
def __getattr__(self, setting_name):
# If the setting you want is filled by the user, let's use it.
if hasattr(user_settings, setting_name):
return getattr(user_settings, setting_name)
# Let's check every local app loaded by django.
for app in get_local_apps():
module_source = os.path.join(app.__path__[0], "%s.py" % self.SETTINGS_MODULE)
module_binary = os.path.join(app.__path__[0], "%s.pyc" % self.SETTINGS_MODULE)
if os.path.exists(module_source) or os.path.exists(module_binary):
module = importlib.import_module("%s.%s" % (app.__name__, self.SETTINGS_MODULE))
# Let's take the first default value for this setting we can find in any app
if hasattr(module, setting_name):
return getattr(module, setting_name)
raise AttributeError("'Settings' object has no attribute '%s'" % setting_name)
settings = AppSettings()
This solution may seem more easier to install, but it does not guarantee that the good default value will be returned. If several applications declare the same variable in their settings.py, you can not be sure which one will return the default value you asked.
Starting from Mike's answer, I now wrapped the default setting handling into a class with easy to use interface.
Helper module:
from django.conf import settings
class SettingsView(object):
class Defaults(object):
pass
def __init__(self):
self.defaults = SettingsView.Defaults()
def __getattr__(self, name):
return getattr(settings, name, getattr(self.defaults, name))
Usage:
from localconf import SettingsView
settings = SettingsView()
settings.defaults.APPLES = 1
print settings.APPLES
This prints the value from django.conf.settings, or the default if it isn't set there. This settings object can also be used to access all the standard setting values.
I recently had the same problem and created a Django app that is designed to be used for exactly such a case. It allows you to define default values for certain settings. It then first checks whether the setting is set in the global settings file. If not, it will return the default value.
I've extended it to also allow for some type checking or pre handling of the default value (e.g. a dotted class path can be converted to the class itself on load)
The app can be found at: https://pypi.python.org/pypi?name=django-pluggableappsettings&version=0.2.0&:action=display

How to mock django settings attribute used in another module?

Lets say module a code:
from django.conf import settings
print settings.BASE_URL # prints http://example.com
In tests.py I want to mock the BASE_URL to http://localhost
I have tried the following:
with mock.patch('django.conf.settings.BASE_URL', 'http://localhost'):
pass
with mock.patch('a.settings.BASE_URL', 'http://localhost'):
pass
from a import settings
with mock.patch.object(settings, 'BASE_URL', 'http://localhost'):
pass
import a
with mock.patch.object(a.settings, 'BASE_URL', 'http://localhost'):
pass
None of the above worked.
Try to use context manager settings() built-in django.
with self.settings(BASE_URL='http://localhost'):
# perform your test
https://docs.djangoproject.com/en/dev/topics/testing/tools/#django.test.SimpleTestCase.settings
You can also use the following decorator on your individual test functions or test class as a whole. For example:
from django.test import override_settings
#override_settings(BASE_URL='http://localhost')
def test_case()
...

Django: How to create a model dynamically just for testing

I have a Django app that requires a settings attribute in the form of:
RELATED_MODELS = ('appname1.modelname1.attribute1',
'appname1.modelname2.attribute2',
'appname2.modelname3.attribute3', ...)
Then hooks their post_save signal to update some other fixed model depending on the attributeN defined.
I would like to test this behaviour and tests should work even if this app is the only one in the project (except for its own dependencies, no other wrapper app need to be installed). How can I create and attach/register/activate mock models just for the test database? (or is it possible at all?)
Solutions that allow me to use test fixtures would be great.
You can put your tests in a tests/ subdirectory of the app (rather than a tests.py file), and include a tests/models.py with the test-only models.
Then provide a test-running script (example) that includes your tests/ "app" in INSTALLED_APPS. (This doesn't work when running app tests from a real project, which won't have the tests app in INSTALLED_APPS, but I rarely find it useful to run reusable app tests from a project, and Django 1.6+ doesn't by default.)
(NOTE: The alternative dynamic method described below only works in Django 1.1+ if your test case subclasses TransactionTestCase - which slows down your tests significantly - and no longer works at all in Django 1.7+. It's left here only for historical interest; don't use it.)
At the beginning of your tests (i.e. in a setUp method, or at the beginning of a set of doctests), you can dynamically add "myapp.tests" to the INSTALLED_APPS setting, and then do this:
from django.core.management import call_command
from django.db.models import loading
loading.cache.loaded = False
call_command('syncdb', verbosity=0)
Then at the end of your tests, you should clean up by restoring the old version of INSTALLED_APPS and clearing the app cache again.
This class encapsulates the pattern so it doesn't clutter up your test code quite as much.
#paluh's answer requires adding unwanted code to a non-test file and in my experience, #carl's solution does not work with django.test.TestCase which is needed to use fixtures. If you want to use django.test.TestCase, you need to make sure you call syncdb before the fixtures get loaded. This requires overriding the _pre_setup method (putting the code in the setUp method is not sufficient). I use my own version of TestCase that lets me add apps with test models. It is defined as follows:
from django.conf import settings
from django.core.management import call_command
from django.db.models import loading
from django import test
class TestCase(test.TestCase):
apps = ()
def _pre_setup(self):
# Add the models to the db.
self._original_installed_apps = list(settings.INSTALLED_APPS)
for app in self.apps:
settings.INSTALLED_APPS.append(app)
loading.cache.loaded = False
call_command('syncdb', interactive=False, verbosity=0)
# Call the original method that does the fixtures etc.
super(TestCase, self)._pre_setup()
def _post_teardown(self):
# Call the original method.
super(TestCase, self)._post_teardown()
# Restore the settings.
settings.INSTALLED_APPS = self._original_installed_apps
loading.cache.loaded = False
I shared my solution that I use in my projects. Maybe it helps someone.
pip install django-fake-model
Two simple steps to create fake model:
1) Define model in any file (I usualy define model in test file near a test case)
from django_fake_model import models as f
class MyFakeModel(f.FakeModel):
name = models.CharField(max_length=100)
2) Add decorator #MyFakeModel.fake_me to your TestCase or to test function.
class MyTest(TestCase):
#MyFakeModel.fake_me
def test_create_model(self):
MyFakeModel.objects.create(name='123')
model = MyFakeModel.objects.get(name='123')
self.assertEqual(model.name, '123')
This decorator creates table in your database before each test and remove the table after test.
Also you may create/delete table manually: MyFakeModel.create_table() / MyFakeModel.delete_table()
I've figured out a way for test-only models for django 1.7+.
The basic idea is, make your tests an app, and add your tests to INSTALLED_APPS.
Here's an example:
$ ls common
__init__.py admin.py apps.py fixtures models.py pagination.py tests validators.py views.py
$ ls common/tests
__init__.py apps.py models.py serializers.py test_filter.py test_pagination.py test_validators.py views.py
And I have different settings for different purposes(ref: splitting up the settings file), namely:
settings/default.py: base settings file
settings/production.py: for production
settings/development.py: for development
settings/testing.py: for testing.
And in settings/testing.py, you can modify INSTALLED_APPS:
settings/testing.py:
from default import *
DEBUG = True
INSTALLED_APPS += ['common', 'common.tests']
And make sure that you have set a proper label for your tests app, namely,
common/tests/apps.py
from django.apps import AppConfig
class CommonTestsConfig(AppConfig):
name = 'common.tests'
label = 'common_tests'
common/tests/__init__.py, set up proper AppConfig(ref: Django Applications).
default_app_config = 'common.tests.apps.CommonTestsConfig'
Then, generate db migration by
python manage.py makemigrations --settings=<your_project_name>.settings.testing tests
Finally, you can run your test with param --settings=<your_project_name>.settings.testing.
If you use py.test, you can even drop a pytest.ini file along with django's manage.py.
py.test
[pytest]
DJANGO_SETTINGS_MODULE=kungfu.settings.testing
Quoting from a related answer:
If you want models defined for testing only then you should check out
Django ticket #7835 in particular comment #24 part of which
is given below:
Apparently you can simply define models directly in your tests.py.
Syncdb never imports tests.py, so those models won't get synced to the
normal db, but they will get synced to the test database, and can be
used in tests.
This solution works only for earlier versions of django (before 1.7). You can check your version easily:
import django
django.VERSION < (1, 7)
Original response:
It's quite strange but form me works very simple pattern:
add tests.py to app which you are going to test,
in this file just define testing models,
below put your testing code (doctest or TestCase definition),
Below I've put some code which defines Article model which is needed only for tests (it exists in someapp/tests.py and I can test it just with: ./manage.py test someapp ):
class Article(models.Model):
title = models.CharField(max_length=128)
description = models.TextField()
document = DocumentTextField(template=lambda i: i.description)
def __unicode__(self):
return self.title
__test__ = {"doctest": """
#smuggling model for tests
>>> from .tests import Article
#testing data
>>> by_two = Article.objects.create(title="divisible by two", description="two four six eight")
>>> by_three = Article.objects.create(title="divisible by three", description="three six nine")
>>> by_four = Article.objects.create(title="divisible by four", description="four four eight")
>>> Article.objects.all().search(document='four')
[<Article: divisible by two>, <Article: divisible by four>]
>>> Article.objects.all().search(document='three')
[<Article: divisible by three>]
"""}
Unit tests also working with such model definition.
I chose a slightly different, albeit more coupled, approach to dynamically creating models just for testing.
I keep all my tests in a tests subdirectory that lives in my files app. The models.py file in the tests subdirectory contains my test-only models. The coupled part comes in here, where I need to add the following to my settings.py file:
# check if we are testing right now
TESTING = 'test' in sys.argv
if TESTING:
# add test packages that have models
INSTALLED_APPS += ['files.tests',]
I also set db_table in my test model, because otherwise Django would have created the table with the name tests_<model_name>, which may have caused a conflict with other test models in another app. Here's my my test model:
class Recipe(models.Model):
'''Test-only model to test out thumbnail registration.'''
dish_image = models.ImageField(upload_to='recipes/')
class Meta:
db_table = 'files_tests_recipe'
Here's the pattern that I'm using to do this.
I've written this method that I use on a subclassed version of TestCase. It goes as follows:
#classmethod
def create_models_from_app(cls, app_name):
"""
Manually create Models (used only for testing) from the specified string app name.
Models are loaded from the module "<app_name>.models"
"""
from django.db import connection, DatabaseError
from django.db.models.loading import load_app
app = load_app(app_name)
from django.core.management import sql
from django.core.management.color import no_style
sql = sql.sql_create(app, no_style(), connection)
cursor = connection.cursor()
for statement in sql:
try:
cursor.execute(statement)
except DatabaseError, excn:
logger.debug(excn.message)
pass
Then, I create a special test-specific models.py file in something like myapp/tests/models.py that's not included in INSTALLED_APPS.
In my setUp method, I call create_models_from_app('myapp.tests') and it creates the proper tables.
The only "gotcha" with this approach is that you don't really want to create the models ever time setUp runs, which is why I catch DatabaseError. I guess the call to this method could go at the top of the test file and that would work a little better.
Combining your answers, specially #slacy's, I did this:
class TestCase(test.TestCase):
initiated = False
#classmethod
def setUpClass(cls, *args, **kwargs):
if not TestCase.initiated:
TestCase.create_models_from_app('myapp.tests')
TestCase.initiated = True
super(TestCase, cls).setUpClass(*args, **kwargs)
#classmethod
def create_models_from_app(cls, app_name):
"""
Manually create Models (used only for testing) from the specified string app name.
Models are loaded from the module "<app_name>.models"
"""
from django.db import connection, DatabaseError
from django.db.models.loading import load_app
app = load_app(app_name)
from django.core.management import sql
from django.core.management.color import no_style
sql = sql.sql_create(app, no_style(), connection)
cursor = connection.cursor()
for statement in sql:
try:
cursor.execute(statement)
except DatabaseError, excn:
logger.debug(excn.message)
With this, you don't try to create db tables more than once, and you don't need to change your INSTALLED_APPS.
If you are writing a reusable django-app, create a minimal test-dedicated app for it!
$ django-admin.py startproject test_myapp_project
$ django-admin.py startapp test_myapp
add both myapp and test_myapp to the INSTALLED_APPS, create your models there and it's good to go!
I have gone through all these answers as well as django ticket 7835, and I finally went for a totally different approach.
I wanted my app (somehow extending queryset.values() ) to be able to be tested in isolation; also, my package does include some models and I wanted a clean distinction between test models and package ones.
That's when I realized it was easier to add a very small django project in the package!
This also allows a much cleaner separation of code IMHO:
In there you can cleanly and without any hack define your models, and you know they will be created when you run your tests from in there!
If you are not writing an independent, reusable app you can still go this way: create a test_myapp app, and add it to your INSTALLED_APPS only in a separate settings_test_myapp.py!
Someone already mentioned Django ticket #7835, but there appears to be a more recent reply that looks much more promising for more recent versions of Django. Specifically #42, which proposes a different TestRunner:
from importlib.util import find_spec
import unittest
from django.apps import apps
from django.conf import settings
from django.test.runner import DiscoverRunner
class TestLoader(unittest.TestLoader):
""" Loader that reports all successful loads to a runner """
def __init__(self, *args, runner, **kwargs):
self.runner = runner
super().__init__(*args, **kwargs)
def loadTestsFromModule(self, module, pattern=None):
suite = super().loadTestsFromModule(module, pattern)
if suite.countTestCases():
self.runner.register_test_module(module)
return suite
class RunnerWithTestModels(DiscoverRunner):
""" Test Runner that will add any test packages with a 'models' module to INSTALLED_APPS.
Allows test only models to be defined within any package that contains tests.
All test models should be set with app_label = 'tests'
"""
def __init__(self, *args, **kwargs):
self.test_packages = set()
self.test_loader = TestLoader(runner=self)
super().__init__(*args, **kwargs)
def register_test_module(self, module):
self.test_packages.add(module.__package__)
def setup_databases(self, **kwargs):
# Look for test models
test_apps = set()
for package in self.test_packages:
if find_spec('.models', package):
test_apps.add(package)
# Add test apps with models to INSTALLED_APPS that aren't already there
new_installed = settings.INSTALLED_APPS + tuple(ta for ta in test_apps if ta not in settings.INSTALLED_APPS)
apps.set_installed_apps(new_installed)
return super().setup_databases(**kwargs)

Categories

Resources