问题:在Python Django中运行单元测试时,如何禁用日志记录?

我正在使用一个基于单元测试的简单测试运行器来测试我的Django应用程序。

我的应用程序本身配置为在settings.py中使用基本记录器,方法是:

logging.basicConfig(level=logging.DEBUG)

在我的应用程序代码中使用:

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

但是,在运行单元测试时,我想禁用日志记录,以免混乱我的测试结果输出。有没有一种简单的方法可以以全局方式关闭日志记录,以便在运行测试时,特定于应用程序的记录器不会将内容写到控制台上?

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?


回答 0

logging.disable(logging.CRITICAL)

将禁用所有级别不低于或等于的日志记录调用CRITICAL。可以通过以下方式重新启用日志记录

logging.disable(logging.NOTSET)
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)

回答 1

由于您使用的是Django,因此可以将以下几行添加到settings.py中:

import sys
import logging

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

这样,您不必setUp()在测试中的每行中都添加该行。

您也可以通过这种方式对测试需求进行一些方便的更改。

还有另一种“更精细”的方法可以为测试添加细节,这就是您自己的测试运行者。

只需创建一个这样的类:

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)

现在添加到您的settings.py文件中:

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

这使您可以进行一种非常方便的修改,而另一种方法则不需要,这就是使Django仅测试所需的应用程序。您可以通过更改test_labels将以下行添加到测试运行器来实现:

if not test_labels:
    test_labels = ['my_app1', 'my_app2', ...]

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', ...]

回答 2

有没有一种简单的方法可以以全局方式关闭日志记录,以便在运行测试时,特定于应用程序的记录器不会将内容写到控制台上?

其他答案通过全局设置日志记录基础结构以忽略任何内容来防止“将内容写到控制台”。这行得通,但我觉得这种方法太钝了。我的方法是执行配置更改,该更改只执行防止日志从控制台中丢失所需的操作。所以我添加了一个自定义的日志过滤器到我的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

将Django日志配置为使用过滤器:

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,
        },
    }
}

最终结果:当我进行测试时,没有任何内容进入控制台,但其他一切保持不变。

为什么这样做?

我设计的代码包含仅在特定情况下触发的日志记录指令,如果出现问题,该指令应输出我诊断所需的确切数据。因此,我测试了他们执行了应做的事情,因此完全禁用日志记录对我而言不可行。我不想在软件投入生产后发现我认为要记录的内容没有记录下来。

此外,一些测试运行程序(例如,Nose)将在测试过程中捕获日志,并输出日志的相关部分以及测试失败。在弄清楚测试失败的原因时很有用。如果日志记录已完全关闭,则无法捕获任何内容。

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.


回答 3

我喜欢Hassek的自定义测试跑步者想法。应该注意的DjangoTestSuiteRunner是,它不再是Django 1.6+中的默认测试运行程序,而是由代替DiscoverRunner。对于默认行为,测试运行器应类似于:

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)

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)

回答 4

我发现对于unittest框架内或类似框架内的测试,安全禁用单元测试中不必要的日志记录的最有效方法是在特定测试用例的setUp/ tearDown方法中启用/禁用。这使一个目标明确地应在哪里禁用日志。您也可以在要测试的类的记录器上明确地执行此操作。

import unittest
import logging

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

    def tearDown(self):
        logging.disable(logging.NOTSET)

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)

回答 5

我正在使用一个简单的方法装饰器来仅在特定的测试方法中禁用日志记录。

def disable_logging(f):

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

        return result

    return wrapper

然后像下面的示例一样使用它:

class ScenarioTestCase(TestCase):

    @disable_logging
    test_scenario(self):
        pass

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

回答 6

有一些漂亮而干净的方法可以挂起使用unittest.mock.patch方法登录测试。

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())

并且python3 -m unittest tests不会产生任何日志输出。

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.


回答 7

有时您需要日志,有时则不需要。我的代码中有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)

因此,如果使用--no-logs选项运行测试,则只会获得critical日志:

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

如果要在持续集成流程中加快测试速度,这将非常有帮助。

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.


回答 8

如果您不希望它在setUp()和tearDown()中反复打开/关闭它以进行单元测试(看不到原因),则每个类只能执行一次:

    import unittest
    import logging

    class TestMyUnitTest(unittest.TestCase):
        @classmethod
        def setUpClass(cls):
            logging.disable(logging.CRITICAL)
        @classmethod
        def tearDownClass(cls):
            logging.disable(logging.NOTSET)

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)

回答 9

如果我想暂时取消某个特定的记录器,我编写了一个有用的小上下文管理器:

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

然后,您可以像这样使用它:

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

这样做的好处是,with完成后将重新启用记录器(或将其设置回其先前的状态)。

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.


回答 10

您可以将其放在单元测试__init__.py文件的顶级目录中。这将禁用单元测试套件中的全局日志记录。

# tests/unit/__init__.py
import logging

logging.disable(logging.CRITICAL)

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)

回答 11

就我而言,我有一个settings/test.py专门为测试目的而创建的设置文件 ,如下所示:

from .base import *

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

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

LOGGING = {}

我把一个环境变量DJANGO_SETTINGS_MODULE=settings.test/etc/environment

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.


回答 12

如果您有用于测试,开发和生产的不同的初始化模块,则可以禁用任何内容或将其重定向到初始化程序中。我有local.py,test.py和production.py,它们都从common.y继承

common.py进行包括以下代码段在内的所有主要配置:

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,
    },
}

然后在test.py我有这个:

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

这用FileHandler代替了控制台处理程序,意味着仍然可以记录日志,但是我不必接触生产代码库。

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.


回答 13

如果您使用的是pytest

由于pytest捕获日志消息并仅在失败的测试中显示它们,因此您通常不希望禁用任何日志记录。相反,请使用单独的settings.py文件进行测试(例如test_settings.py),然后添加到其中:

LOGGING_CONFIG = None

这告诉Django完全跳过配置日志记录。的LOGGING设置将被忽略,可以从设置中删除。

使用这种方法,对于通过的测试,您将不会获得任何日志记录,对于失败的测试,您将获得所有可用的日志记录。

测试将使用由设置的日志记录运行pytest。您可以根据自己的喜好来配置它pytest(例如tox.ini)。要包括调试级别日志消息,请使用log_level = DEBUG(或相应的命令行参数)。

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).


声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。