223

I am using a simple unit test based test runner to test my Django application.

My application itself is configured to use a basic logger in settings.py using:

logging.basicConfig(level=logging.DEBUG)

And in my application code using:

logger = logging.getLogger(__name__)
logger.setLevel(getattr(settings, 'LOG_LEVEL', logging.DEBUG))

However, when running unittests, I'd like to disable logging so that it doesn't clutter my test result output. Is there a simple way to turn off logging in a global way, so that the application specific loggers aren't writing stuff out to the console when I run tests?

vvvvv
  • 25,404
  • 19
  • 49
  • 81
shreddd
  • 10,975
  • 9
  • 33
  • 34
  • 1
    How did you enable logging whilst running tests? and why aren't you using django LOGGING? – dalore May 19 '15 at 11:58

20 Answers20

318
logging.disable(logging.CRITICAL)

will disable all logging calls with levels less severe than or equal to CRITICAL. Logging can be re-enabled with

logging.disable(logging.NOTSET)
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • in the setUp() method of your test, or in the actual test that generates the log messages that you want to hide. – qris Jan 09 '13 at 15:15
  • 12
    And in your `tearDown()` method: `logging.disable(logging.NOTSET)` puts the logging back in place neatly. – mlissner May 07 '13 at 16:27
  • 43
    Putting it in the __init__.py of the `tests` module is very useful. – toabi May 15 '13 at 15:13
  • 54
    This may be obvious but I find it helpful to sometimes state the obvious for the benefit of other readers: You would put the call to `logging.disable` (from the accepted answer) at the top of `tests.py` in your application that is doing the logging. – CJ Gaconnet Apr 06 '11 at 15:51
  • 8
    I ended up putting the call in setUp() but your point is well taken. – shreddd Apr 06 '11 at 20:49
  • Here is the documentation link for `logging.disable(lvl)` for a proper explanation. https://docs.python.org/2/library/logging.html#logging.disable – Andi Feb 09 '18 at 15:41
  • One can also use `setUpModule` and `tearDownModule` to disable and enable the logging, respectively. – Rafael Beirigo May 19 '21 at 17:37
58

Since you are in Django, you could add these lines to your settings.py:

import sys
import logging

if len(sys.argv) > 1 and sys.argv[1] == 'test':
    logging.disable(logging.CRITICAL)

That way you don't have to add that line in every setUp() on your tests.

You could also do a couple of handy changes for your test needs this way.

There is another "nicer" or "cleaner" way to add specifics to your tests and that is making your own test runner.

Just create a class like this:

import logging

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

class MyOwnTestRunner(DjangoTestSuiteRunner):
    def run_tests(self, test_labels, extra_tests=None, **kwargs):

        # Don't show logging messages while testing
        logging.disable(logging.CRITICAL)

        return super(MyOwnTestRunner, self).run_tests(test_labels, extra_tests, **kwargs)

And now add to your settings.py file:

TEST_RUNNER = "PATH.TO.PYFILE.MyOwnTestRunner"
#(for example, 'utils.mytest_runner.MyOwnTestRunner')

This lets you do one really handy modification that the other approach doesn't, which is to make Django just tests the applications that you want. You can do that by changing the test_labels adding this line to the test runner:

if not test_labels:
    test_labels = ['my_app1', 'my_app2', ...]
vvvvv
  • 25,404
  • 19
  • 49
  • 81
Hassek
  • 8,715
  • 6
  • 47
  • 59
28

Is there a simple way to turn off logging in a global way, so that the application specific loggers aren't writing stuff out to the console when I run tests?

The other answers prevent "writing stuff out to the console" by globally setting the logging infrastructure to ignore anything. This works but I find it too blunt an approach. My approach is to perform a configuration change which does only what's needed to prevent logs to get out on the console. So I add a custom logging filter to my settings.py:

from logging import Filter

class NotInTestingFilter(Filter):

    def filter(self, record):
        # Although I normally just put this class in the settings.py
        # file, I have my reasons to load settings here. In many
        # cases, you could skip the import and just read the setting
        # from the local symbol space.
        from django.conf import settings

        # TESTING_MODE is some settings variable that tells my code
        # whether the code is running in a testing environment or
        # not. Any test runner I use will load the Django code in a
        # way that makes it True.
        return not settings.TESTING_MODE

And I configure the Django logging to use the filter:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'testing': {
            '()': NotInTestingFilter
        }
    },
    'formatters': {
        'verbose': {
            'format': ('%(levelname)s %(asctime)s %(module)s '
                       '%(process)d %(thread)d %(message)s')
        },
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'filters': ['testing'],
            'formatter': 'verbose'
        },
    },
    'loggers': {
        'foo': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': True,
        },
    }
}

End result: when I'm testing, nothing goes to the console, but everything else stays the same.

Why Do This?

I design code that contains logging instructions that are triggered only in specific circumstances and that should output the exact data I need for diagnosis if things go wrong. Therefore I test that they do what they are supposed to do and thus completely disabling logging is not viable for me. I don't want to find once the software is in production that what I thought would be logged is not logged.

Moreover, some test runners (Nose, for instance) will capture logs during testing and output the relevant part of the log together with a test failure. It is useful in figuring out why a test failed. If logging is completely turned off, then there's nothing that can be captured.

bignose
  • 30,281
  • 14
  • 77
  • 110
Louis
  • 146,715
  • 28
  • 274
  • 320
  • "Any test runner I use will load the Django code in a way that makes it True." Interesting... How? – webtweakers Mar 17 '16 at 10:24
  • I have a `test_settings.py` file which sits next to my project's `settings.py`. It is set to load `settings.py` and make some changes like set `TESTING_MODE` to `True`. My test runners are organized so that `test_settings` is the module loaded for the Django project settings. There are many ways this can be done. I usually go with setting the environment variable `DJANGO_SETTINGS_MODULE` to `proj.test_settings`. – Louis Mar 18 '16 at 10:44
  • This is awesome and does exactly what I want. Hides the logging during unittests until something fails -- then Django Nose picks up the output and prints it with the failure. Perfect. Combine it with [this](http://stackoverflow.com/a/6960879/2077386) to determine whether unit testing is active. – rrauenza Jul 28 '16 at 17:30
  • I just wasted several hours trying to figure out why my `assertLogs` was failing and it was due to `logging.disable(logging.CRITICAL)`. Just turning off console output in some way is FAR better than totally breaking logging. – Tim Tisdall Dec 02 '21 at 13:52
  • There is no built-in `TESTING_MODE` (I just checked), so this need to be modified slightly. Could add the following into settings.py: `TESTING_MODE = 'test' in sys.argv`. – Tim Tisdall Dec 02 '21 at 14:14
21

I like Hassek's custom test runner idea. It should be noted that DjangoTestSuiteRunner is no longer the default test runner in Django 1.6+, it has been replaced by the DiscoverRunner. For default behaviour, the test runner should be more like:

import logging

from django.test.runner import DiscoverRunner

class NoLoggingTestRunner(DiscoverRunner):
    def run_tests(self, test_labels, extra_tests=None, **kwargs):

        # disable logging below CRITICAL while testing
        logging.disable(logging.CRITICAL)

        return super(NoLoggingTestRunner, self).run_tests(test_labels, extra_tests, **kwargs)
alukach
  • 5,921
  • 3
  • 39
  • 40
  • I found your solution after trying a lot of things. However I am not able to set the variable TEST_RUNNER in settings as its not able to import the module where the test_runner file is. – Bunny Rabbit May 15 '15 at 17:51
  • Sounds like an import issue. Are you setting TEST_RUNNER to a string path to the runner (not the actual Python module)? Also, where is your runner located? I have mine in a separate app named `helpers`, which only has utils that don't import from anywhere else within the project. – alukach May 15 '15 at 18:23
15

I've found that for tests within unittest or similar a framework, the most effective way to safely disable unwanted logging in unit tests is to enable/disable in the setUp/tearDown methods of a particular test case. This lets one target specifically where logs should be disabled. You could also do this explicitly on the logger of the class you're testing.

import unittest
import logging

class TestMyUnitTest(unittest.TestCase):
    def setUp(self):
        logging.disable(logging.CRITICAL)

    def tearDown(self):
        logging.disable(logging.NOTSET)
mcguip
  • 5,947
  • 5
  • 25
  • 32
9

I am using a simple method decorator to disable logging only in a particular test method.

def disable_logging(f):

    def wrapper(*args):
        logging.disable(logging.CRITICAL)
        result = f(*args)
        logging.disable(logging.NOTSET)

        return result

    return wrapper

And then I use it as in the following example:

class ScenarioTestCase(TestCase):

    @disable_logging
    test_scenario(self):
        pass
Eduard Mukans
  • 857
  • 8
  • 11
8

There is some pretty and clean method to suspend logging in tests with unittest.mock.patch method.

foo.py:

import logging


logger = logging.getLogger(__name__)

def bar():
    logger.error('There is some error output here!')
    return True

tests.py:

from unittest import mock, TestCase
from foo import bar


class FooBarTestCase(TestCase):
    @mock.patch('foo.logger', mock.Mock())
    def test_bar(self):
        self.assertTrue(bar())

And python3 -m unittest tests will produce no logging output.

valex
  • 5,163
  • 2
  • 33
  • 40
3

Sometimes you want the logs and sometimes not. I have this code in my settings.py

import sys

if '--no-logs' in sys.argv:
    print('> Disabling logging levels of CRITICAL and below.')
    sys.argv.remove('--no-logs')
    logging.disable(logging.CRITICAL)

So if you run your test with the --no-logs options you'll get only the critical logs:

$ python ./manage.py tests --no-logs
> Disabling logging levels of CRITICAL and below.

It's very helpful if you want speedup the tests on your continuous integration flow.

Karim N Gorjux
  • 2,880
  • 22
  • 29
  • This should be in `manage.py`, not in `settings.py` – DanielM Oct 24 '21 at 16:41
  • I like this answer a lot, because it let you control the log level in the command line. This is useful when disabling logging for CI but keeping it enabled for running local tests. Thanks a lot. – flix Nov 17 '21 at 18:22
3

In cases where I wish to temporarily suppress a specific logger, I've written a little context manager that I've found useful:

from contextlib import contextmanager
import logging

@contextmanager
def disable_logger(name):
    """Temporarily disable a specific logger."""
    logger = logging.getLogger(name)
    old_value = logger.disabled
    logger.disabled = True
    try:
        yield
    finally:
        logger.disabled = old_value

You then use it like:

class MyTestCase(TestCase):
    def test_something(self):
        with disable_logger('<logger name>'):
            # code that causes the logger to fire

This has the advantage that the logger is re-enabled (or set back to its prior state) once the with completes.

Nathan Villaescusa
  • 17,331
  • 4
  • 53
  • 56
3

Disable logging for the entire module:

import logging


def setUpModule():
    """Disable logging while doing these tests."""
    logging.disable()


def tearDownModule():
    """Re-enable logging after doing these tests."""
    logging.disable(logging.NOTSET)


class TestFoo(unittest.TestCase):

    def test_foo(self):
        pass
Rafael Beirigo
  • 1,552
  • 1
  • 13
  • 11
2

If you're using pytest:

Since pytest captures log messages and only displays them for failed tests, you typically don't want to disable any logging. Instead, use a separate settings.py file for tests (e.g., test_settings.py), and add to it:

LOGGING_CONFIG = None

This tells Django to skip configuring the logging altogether. The LOGGING setting will be ignored and can be removed from the settings.

With this approach, you don't get any logging for passed tests, and you get all available logging for failed tests.

The tests will run using the logging that was set up by pytest. It can be configured to your liking in the pytest settings (e.g., tox.ini). To include debug level log messages, use log_level = DEBUG (or the corresponding command line argument).

Roger Dahl
  • 15,132
  • 8
  • 62
  • 82
2

You can put this in the top level directory for unit tests __init__.py file. This will disable logging globally in the unit test suite.

# tests/unit/__init__.py
import logging

logging.disable(logging.CRITICAL)
Aaron Lelevier
  • 19,850
  • 11
  • 76
  • 111
2

Some of my tests contain assertions about log output, so changing the log level breaks them. Instead, I changed my Django LOGGING settings to use a NullHandler when running tests:

if 'test' in sys.argv:
    _LOG_HANDLERS = ['null']
else:
    _LOG_HANDLERS = ['console']
    
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simple': {
            'format': '%(levelname)s %(message)s'
        },
    },
    'handlers': {
        'null': {
            'level': 'DEBUG',
            'class': 'logging.NullHandler',
        },
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
        },
    },
    'loggers': {
        'django': {
            'handlers': _LOG_HANDLERS,
            'propagate': True,
            'level': 'INFO',
        },
    }
}
Leila Hadj-Chikh
  • 1,653
  • 17
  • 16
1

If you don't want it repeatedly turn it on/off in setUp() and tearDown() for unittest (don't see the reason for that), you could just do it once per class:

    import unittest
    import logging

    class TestMyUnitTest(unittest.TestCase):
        @classmethod
        def setUpClass(cls):
            logging.disable(logging.CRITICAL)
        @classmethod
        def tearDownClass(cls):
            logging.disable(logging.NOTSET)
the pillow
  • 412
  • 6
  • 14
1

b.h.

for me works whell - in conftest.py:

 import confing
 # disable logging for tests
 [logging.disable(level) for level in [logging.DEBUG,
                                       logging.INFO,
                                       logging.ERROR,
                                       logging.CRITICAL]]
zushe
  • 153
  • 1
  • 7
0

In my case I have a settings file settings/test.py created specifically for testing purposes, here's what it looks like:

from .base import *

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'test_db'
    }
}

PASSWORD_HASHERS = (
    'django.contrib.auth.hashers.MD5PasswordHasher',
)

LOGGING = {}

I put an environment variable DJANGO_SETTINGS_MODULE=settings.test to /etc/environment.

Dmitrii Mikhailov
  • 5,053
  • 7
  • 43
  • 69
0

If you have different initaliser modules for test, dev and production then you can disable anything or redirect it in the initialser. I have local.py, test.py and production.py that all inherit from common.y

common.py does all the main config including this snippet :

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
    'django.server': {
        '()': 'django.utils.log.ServerFormatter',
        'format': '[%(server_time)s] %(message)s',
    },
    'verbose': {
        'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
    },
    'simple': {
        'format': '%(levelname)s %(message)s'
    },
},
'filters': {
    'require_debug_true': {
        '()': 'django.utils.log.RequireDebugTrue',
    },
},
'handlers': {
    'django.server': {
        'level': 'INFO',
        'class': 'logging.StreamHandler',
        'formatter': 'django.server',
    },
    'console': {
        'level': 'DEBUG',
        'class': 'logging.StreamHandler',
        'formatter': 'simple'
    },
    'mail_admins': {
        'level': 'ERROR',
        'class': 'django.utils.log.AdminEmailHandler'
    }
},
'loggers': {
    'django': {
        'handlers': ['console'],
        'level': 'INFO',
        'propagate': True,
    },
    'celery.tasks': {
        'handlers': ['console'],
        'level': 'DEBUG',
        'propagate': True,
    },
    'django.server': {
        'handlers': ['django.server'],
        'level': 'INFO',
        'propagate': False,
    },
}

Then in test.py I have this:

console_logger = Common.LOGGING.get('handlers').get('console')
console_logger['class'] = 'logging.FileHandler
console_logger['filename'] = './unitest.log

This replaces the console handler with a FileHandler and means still get logging but I do not have to touch the production code base.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
0

In case you're using pytest, you can install the super useful pytest-mock plugin and define an auto-used, session-scoped fixture that can be triggered by an env var:

# conftest.py

import os
import pytest


@pytest.fixture(autouse=True, scope="session")
def _shut_logger(session_mocker):
    if os.getenv("SHUT_LOGGER", None):
        return session_mocker.patch("foo.logger")
scorphus
  • 1
  • 3
0

We use structlog, which is a bit more complicated to disable:

from structlog import DropEvent

def disable_logging_in_tests(_, __, event_dict):
    if len(sys.argv) > 1 and sys.argv[1] == 'test':
        raise DropEvent
    return event_dict


structlog.configure(
    processors=[
        ...
        disable_logging_in_tests,
    ]
    ...

)
flix
  • 1,821
  • 18
  • 23
-2

You're likely asking the wrong question if you're wondering about this in 2021 or later

On modern versions of Django*, with the out of the box configuration, tests should not produce any logging on screen. Thus, if you're aksing this question the real answer may be "something is misconfigured". This is because (by default):

Thus, if the loggers you use match those you have defined in LOGGING['loggers'] and being handled by the "console" handler, tests should not produce any logging on screen.

If you see something in tests anyway, you either

  • have mismatched the name of your logger with the thing you've defined in settings.LOGGING
  • are running tests with DEBUG=True (which requires an override)
  • have removed "require_debug_true" from your console handler's filters.

*Modern versions meaning: 2.1 and up, i.e. not ancient.

Klaas van Schelven
  • 2,374
  • 1
  • 21
  • 35
  • 1
    One point, though... The way Django documents setting up `LOGGING` is to define it completely and that overwrites the default set up. A person would need to make sure they redefined the `'console'` handler in the same way so that it still has a filter to stop output when `DEBUG=False`. – Tim Tisdall Dec 02 '21 at 15:00
  • @TimTisdall that is correct and a great addition. I forgot about that because I tend to set up my `LOGGING` by doing a deepcopy of the default and only changing those parts that I want to have changed. – Klaas van Schelven Dec 07 '21 at 12:49