9

Recently I am writting an python logging extension, and I want to add some tests for my extension to verify whether my extension work as expected.
However, I don't know how to capture the complete log and compare with my excepted result in unittest/pytest.

simplified sample:

# app.py
import logging
def create_logger():
    formatter = logging.Formatter(fmt='%(name)s-%(levelname)s-%(message)s')
    hdlr = logging.StreamHandler()
    hdlr.setFormatter(formatter)
    logger = logging.getLogger(__name__)
    logger.setLevel('DEBUG')
    logger.addHandler(hdlr)
    return logger


app_logger = create_logger()

Here is my tests

Attempt 1: unittest

from app import app_logger
import unittest


class TestApp(unittest.TestCase):

    def test_logger(self):
        with self.assertLogs('', 'DEBUG') as cm:
            app_logger.debug('hello')
        # or some other way to capture the log output.
        self.assertEqual('app-DEBUG-hello', cm.output)
  • expected behaviour:
    cm.output = 'app-DEBUG-hello'
    
  • actual behaviour
    cm.output = ['DEBUG:app:hello']
    

Attempt 2: pytest caplog

from app import app_logger
import pytest


def test_logger(caplog):
    app_logger.debug('hello')
    assert caplog.text == 'app-DEBUG-hello'
  • expected behaviour:
    caplog.text = 'app-DEBUG-hello'
    
  • actual behaviour
    caplog.text = 'test_logger.py               6 DEBUG    hello'
    

Attempt 3: pytest capsys

from app import app_logger import pytest

def test_logger(capsys):
    app_logger.debug('hello')
    out, err = capsys.readouterr()
    assert err
    assert err == 'app-DEBUG-hello'
  • expected behaviour:
    err = 'app-DEBUG-hello'
    
  • actual behaviour
    err = ''
    

Considering there will be many tests with different format, I don't want to check the log format manually. I have no idea how to get complete log as I see on the console and compare it with my expected one in the test cases. Hoping for your help, thx.

4
  • 1
    That's because your handler will be ignored when the emitted log records are captured, both by unittest and pytest. As for enforcing custom formatting, the support is either missing completely (unittest doesn't support custom formatters at all) or pretty limited (with pytest, you can at least pass you custom format string via command line: pytest --log-format="%(name)s-%(levelname)s-%(message)s", but custom formatter classes will be ignored as well). Commented Jan 10, 2019 at 10:54
  • 1
    However, testing the format of collected records throughout the program rarely brings profit anyway; split your tests into: ones that check whether the log records are emitted correctly (so e.g. you don't miss a record when running some function) and ones that validate your custom handlers and formatters (create a record, call handler.emit(record)/formatter.format(record) explicilty and check whether they did their job right). Commented Jan 10, 2019 at 11:00
  • 1
    @hoefling Got it. Thanks for your quickly reply :) What my extension mainly do is addding some extra field to log and formating it with specified format. It seems that it is a little diffcult to verify its format, but you provide me another way to achieve it. I will have a try. Thank you. Commented Jan 11, 2019 at 3:32
  • I am also facing the similar issue right now. Any permanent solution for it by using assertLogs itself? Commented Sep 9, 2022 at 12:55

4 Answers 4

4

I know this is old but posting here since it pulled up in google for me...

Probably needs cleanup but it is the first thing that has gotten close for me so I figured it would be good to share.

Here is a test case mixin I've put together that lets me verify a particular handler is being formatted as expected by copying the formatter:

import io
import logging

from django.conf import settings
from django.test import SimpleTestCase
from django.utils.log import DEFAULT_LOGGING

class SetupLoggingMixin:
    def setUp(self):
        super().setUp()
        logging.config.dictConfig(settings.LOGGING)
        self.stream = io.StringIO()
        self.root_logger = logging.getLogger("")
        self.root_hdlr = logging.StreamHandler(self.stream)

        console_handler = None
        for handler in self.root_logger.handlers:
            if handler.name == 'console':
                console_handler = handler
                break

        if console_handler is None:
            raise RuntimeError('could not find console handler')

        formatter = console_handler.formatter
        self.root_formatter = formatter
        self.root_hdlr.setFormatter(self.root_formatter)
        self.root_logger.addHandler(self.root_hdlr)

    def tearDown(self):
        super().tearDown()
        self.stream.close()
        logging.config.dictConfig(DEFAULT_LOGGING)

And here is an example of how to use it:

class SimpleLogTests(SetupLoggingMixin, SimpleTestCase):
    def test_logged_time(self):
        msg = 'foo'
        self.root_logger.error(msg)
        self.assertEqual(self.stream.getvalue(), 'my-expected-message-formatted-as-expected')
Sign up to request clarification or add additional context in comments.

Comments

2

After reading the source code of the unittest library, I've worked out the following bypass. Note, it works by changing a protected member of an imported module, so it may break in future versions.

from unittest.case import _AssertLogsContext
_AssertLogsContext.LOGGING_FORMAT = 'same format as your logger'

After these commands the logging context opened by self.assertLogs will use the above format. I really don't know why this values is left hard-coded and not configurable.

I did not find an option to read the format of a logger, but if you use logging.config.dictConfig you can use a value from the same dictionary.

Comments

0

I know this doesn't completely answer the OP's question but I stumbled upon this post while looking for a neat way to capture logged messages.

Taking what @user319862 did, I've cleaned it and simplified it.

import unittest
import logging

from io import StringIO

class SetupLogging(unittest.TestCase):
    def setUp(self):
        super().setUp()
        self.stream = StringIO()
        self.root_logger = logging.getLogger("")
        self.root_hdlr = logging.StreamHandler(self.stream)
        self.root_logger.addHandler(self.root_hdlr)

    def tearDown(self):
        super().tearDown()
        self.stream.close()

    def test_log_output(self):
        """ Does the logger produce the correct output? """
        msg = 'foo'
        self.root_logger.error(msg)
        self.assertEqual(self.stream.getvalue(), 'foo\n')


if __name__ == '__main__':
    unittest.main()

Comments

0

I new to python but have some experience in test/tdd in other languages, and found that the default way of "changing" the formatter is by adding a new streamhandler BUT in the case you already have a stream defined in your logger (i.e. using Azure functions or TestCase::assertLogs that add one for you) you end up logging twice one with your format and another with the "default" format.

If in the OP the function create_logger mutates the formatter of current StreamHandler, instead of adding a new StreamHandler (checks if exist and if doesn't creates a new one and all that jazz...)

Then you can call the create_logger after the with self.assertLogs('', 'DEBUG') as cm: and just assert the cm.output and it just works because you are mutating the Formatter of the StreamHandler that the assertLogs is adding.

So basically what's happening is that the execution order is not appropriate for the test.

The order of execution in OP is:

  • import stuff
    • Add stream to logger formatter
  • Run test
    • Add another stream to logger formatter via self.assertLogs
    • assert stuff in 2nd StreamHandler

When it should be the order of execution is:

  • import stuff
  • Add stream with logger formatter (but is irrelevant)
  • Run test
  • Add another stream with logger formatter via self.assertLogs
  • Change current stream logger formatter
  • assert stuff in only and properly formatted StreamHandler

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.