66

How can I detect whether a view is being called in a test environment (e.g., from manage.py test)?

#pseudo_code
def my_view(request):
    if not request.is_secure() and not TEST_ENVIRONMENT:
        return HttpResponseForbidden()
Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
tback
  • 11,138
  • 7
  • 47
  • 71
  • What attributes does `request` have? Is there any indication in there? – Dominic Rodger Nov 03 '10 at 15:11
  • it's worth noting, you could create a `@https_only` wrapper for secure views, instead of using manual logic in your views. In `https_only` you can send a redirect using `https` when needed, or raise your exception there. – orokusaki Feb 16 '11 at 21:52
  • 5
    When you say 'test environment', do you mean 'while running tests'? If so, why on Earth would you want to do this? Special-casing code that only works while you're running tests means that you're not actually testing your real code at all, so what's the point? – Daniel Roseman Nov 03 '10 at 15:11

9 Answers9

139

Put this in your settings.py:

import sys

TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'

This tests whether the second commandline argument (after ./manage.py) was test. Then you can access this variable from other modules, like so:

from django.conf import settings

if settings.TESTING:
    ...

There are good reasons to do this: suppose you're accessing some backend service, other than Django's models and DB connections. Then you might need to know when to call the production service vs. the test service.

Tobia
  • 17,856
  • 6
  • 74
  • 93
  • What if there is some management command which requires the parameter `test` to be put in sys.argv? – Emanuele Paolini Jan 06 '15 at 16:39
  • @EmanuelePaolini in that case you can be more specific and require it to be the 2nd argument (after manage.py): `TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'` – Tobia Jan 06 '15 at 19:13
  • Yes, this is what I'm doing and I would suggest to do in general. – Emanuele Paolini Jan 07 '15 at 11:13
  • 9
    That's a fragile hack, what about someone using nose or py.test to run tests ? – Jocelyn delalande Sep 02 '15 at 10:29
  • @Jocelyndelalande I don't know what nose or py.test are. Any patches are welcome. – Tobia Sep 02 '15 at 11:03
  • 4
    @Tobia they are alternate test runners, broadly used see https://nose.readthedocs.org/en/latest/ and http://pytest.org/latest/ I don't want to patch, I think the approach of looking at argv is not robust and universal enough. Other solutions are best suited IMHO (like travis-jensen rednaw or pymarco). – Jocelyn delalande Sep 02 '15 at 13:05
  • I'm not sure if this is the right answer, but it might take the problem of knowing in which position the test parameter goes: TESTING='test' in sys.argv – Aquiles Sep 18 '15 at 16:30
24

Create your own TestSuiteRunner subclass and change a setting or do whatever else you need to for the rest of your application. You specify the test runner in your settings:

TEST_RUNNER = 'your.project.MyTestSuiteRunner'

In general, you don't want to do this, but it works if you absolutely need it.

from django.conf import settings
from django.test.simple import DjangoTestSuiteRunner

class MyTestSuiteRunner(DjangoTestSuiteRunner):
    def __init__(self, *args, **kwargs):
        settings.IM_IN_TEST_MODE = True
        super(MyTestSuiteRunner, self).__init__(*args, **kwargs)
Travis Jensen
  • 5,362
  • 3
  • 36
  • 40
  • Seems like the only solution here that doesn't rely on knowing some variable value, like sys.argv or the name of the test server... – Mikhail May 11 '13 at 04:51
  • 2
    While it's good to have different solutions to the problem, changing django settings at runtime doesn't seem a wise choice – glarrain Jun 11 '13 at 22:33
  • 1
    This is the most stable, universal and flexible solution. But django.test.simple is deprecated. Should use now from django.test.runner.DiscoverRunner as parent class for test runner – der_fenix Aug 23 '14 at 12:34
  • 1
    Also it is better to change settings in `setup_test_environment` method instead of `__init__` – der_fenix Aug 23 '14 at 12:37
  • 3
    In Django 1.8+, use `from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner` – Amir Ali Akbari Jan 09 '17 at 08:55
15

Just look at request.META['SERVER_NAME']

def my_view(request):
    if request.META['SERVER_NAME'] == "testserver":
        print "This is test environment!"
  • 2
    I wouldn't rely on something that an attacker can have access to, I assume this is somehow configurable through headers and could be manipulated to make believe the origin is from a test suite when it would actually be something external – Vadorequest Apr 13 '19 at 09:54
8

There's also a way to temporarily overwrite settings in a unit test in Django. This might be a easier/cleaner solution for certain cases.

You can do this inside a test:

with self.settings(MY_SETTING='my_value'):
    # test code

Or add it as a decorator on the test method:

@override_settings(MY_SETTING='my_value')
def test_my_test(self):
    # test code

You can also set the decorator for the whole test case class:

@override_settings(MY_SETTING='my_value')
class MyTestCase(TestCase):
    # test methods

For more info check the Django docs: https://docs.djangoproject.com/en/1.11/topics/testing/tools/#django.test.override_settings

gitaarik
  • 42,736
  • 12
  • 98
  • 105
  • 1
    I tried all 3 of these methods on django 2.2 and none appear to work (EDIT: until I realized I was importing myapp.settings instead of django.conf.settings). – bparker Apr 17 '20 at 19:30
6

I think the best approach is to run your tests using their own settings file (i.e. settings/tests.py). That file can look like this (the first line imports settings from a local.py settings file):

from local import *
TEST_MODE = True

Then do ducktyping to check if you are in test mode.

try:
    if settings.TEST_MODE:
        print 'foo'
except AttributeError:
    pass
pymarco
  • 7,807
  • 4
  • 29
  • 40
3

If you are multiple settings file for different environment, all you need to do is to create one settings file for testing.

For instance, your setting files are:

your_project/
      |_ settings/
           |_ __init__.py
           |_ base.py  <-- your original settings
           |_ testing.py  <-- for testing only

In your testing.py, add a TESTING flag:

from .base import *

TESTING = True

In your application, you can access settings.TESTING to check if you're in testing environment.

To run tests, use:

python manage.py test --settings your_project.settings.testing
melvin
  • 81
  • 3
2

While there's no official way to see whether we're in a test environment, django actually leaves some clues for us. By default Django’s test runner automatically redirects all Django-sent email to a dummy outbox. This is accomplished by replacing EMAIL_BACKEND in a function called setup_test_environment, which in turn is called by a method of DiscoverRunner. So, we can check whether settings.EMAIL_BACKEND is set to 'django.core.mail.backends.locmem.EmailBackend'. That mean we're in a test environment.

A less hacky solution would be following the devs lead by adding our own setting by subclassing DisoverRunner and then overriding setup_test_environment method.

sg2002
  • 121
  • 3
1

Piggybacking off of @Tobia's answer, I think it is better implemented in settings.py like this:

import sys
try:
    TESTING = 'test' == sys.argv[1]
except IndexError:
    TESTING = False

This will prevent it from catching things like ./manage.py loaddata test.json or ./manage.py i_am_not_running_a_test

Cory
  • 22,772
  • 19
  • 94
  • 91
  • 2
    This is not very flexible as the `test` argument can be on another index. E.g. run as `python manage.py test` it's at index 2. – gertvdijk Apr 16 '13 at 12:04
  • @gertvdijk then change it to `'test' == sys.argv[2]` :) – glarrain Jun 11 '13 at 22:32
  • 2
    @glarrain Of course, but my point is, that you don't know it on beforehand as it depends on how the user invokes the test runner. – gertvdijk Jun 12 '13 at 08:37
  • 2
    "This will prevent it from catching things like `./manage.py loaddata test.json`". That command wouldn't cause `'test' in sys.argv` to be true anyway since `'test' in ['./manage.py', 'loaddata', 'test.json'] == False`. – Riley Watkins Jul 01 '13 at 20:06
  • All of this is `False` in Tobia's answer either. – Torsten Bronger Aug 14 '14 at 13:16
0

I wanted to exclude some data migrations from being run in tests, and came up with this solution on a Django 3.2 project:

class Migration(migrations.Migration):
    def apply(self, project_state, schema_editor, collect_sql=False):
        import inspect
        if 'create_test_db' in [i.function for i in inspect.stack()]:
            return project_state
        else:
            return super().apply(project_state, schema_editor, collect_sql=collect_sql)

I haven't seen this suggested elsewhere, and for my purposes it's pretty clean. Of course, it might break if Django changes the name of the create_test_db method (or the return value of the apply method) at some point in time, but modifying this to work should be reasonably simple, since it's likely that some method exists in the stack that doesn't exist during non-test migration runs.

JK Laiho
  • 3,538
  • 6
  • 35
  • 42