60

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
  • 22,937
  • 20
  • 77
  • 112
tback
  • 10,210
  • 7
  • 40
  • 69
  • 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

8 Answers8

133

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
  • 16,563
  • 4
  • 66
  • 86
  • 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
  • 8
    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
  • 3
    @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
23

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,242
  • 3
  • 35
  • 38
  • 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
  • 1
    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
  • 36,986
  • 11
  • 91
  • 97
  • 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,117
  • 4
  • 26
  • 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
  • 31
  • 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
  • 20,624
  • 18
  • 92
  • 88
  • 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