Testing with Loguru¶
Note
To ensure you are using compatible versions, install with the testfixtures[loguru] extra.
Loguru provides a streamlined logging API through a single
global Logger object.
LoguruSource is provided to capture and test using
LogCapture.
As a simple example, you can capture loguru output with a pytest fixture such as this:
import pytest
from testfixtures import LogCapture
from testfixtures.loguru import LoguruSource
@pytest.fixture()
def logs():
with LogCapture(LoguruSource()) as logs_:
yield logs_
You can then check logging in your tests like this:
from loguru import logger
def test_logging(logs: LogCapture) -> None:
logger.info('42 is fine')
logger.error('13 is not')
logs.check(
('INFO', '42 is fine'),
('ERROR', '13 is not'),
)
See Testing logging for the full LogCapture API, including
check(), check_present(),
actual(), and the entries
attribute.
Inspecting raw records¶
Each Entry in entries
exposes the underlying loguru record dict via its raw attribute:
from testfixtures import LogCapture
from testfixtures.loguru import LoguruSource
with LogCapture(LoguruSource()) as log:
logger.info('hello world')
>>> log.entries[0].raw
{..., 'level': (name='INFO', ..., 'message': 'hello world', ...}
The loguru record dict contains rich contextual information.
Checking logging context¶
Loguru supports structured context via bind(), which creates a
logger with fixed extra fields, and contextualize(), which
temporarily adds context for all logging within a block. Both are exposed through the extra
key in the record dict.
To capture extra alongside the default level and message, include it in the attributes:
from testfixtures.loguru import LoguruSource, level_name
request_log = logger.bind(request_id='abc123')
with LogCapture(LoguruSource((level_name, 'message', 'extra'))) as log:
request_log.info('handling request')
request_log.info('request complete')
log.check(
('INFO', 'handling request', {'request_id': 'abc123'}),
('INFO', 'request complete', {'request_id': 'abc123'}),
)
contextualize() works the same way but scopes the context to a
with block, affecting all loggers:
with LogCapture(LoguruSource((level_name, 'message', 'extra'))) as log:
logger.info('before task')
with logger.contextualize(task_id=1234):
logger.info('processing task')
logger.info('after task')
log.check(
('INFO', 'before task', {}),
('INFO', 'processing task', {'task_id': 1234}),
('INFO', 'after task', {})
)
Capturing specific fields¶
You can control which fields form the actual value by passing sequence of attributes to
LoguruSource. Elements may be string keys into the
record dict or callables that receive the record dict.
To capture only the message:
with LogCapture(LoguruSource('message')) as log:
logger.info('just the message')
log.check('just the message')
You can also mix string keys and callables:
with LogCapture(LoguruSource((lambda r: r['level'].name, 'message'))) as log:
logger.debug('a debug message')
logger.info('something info')
log.check(
('DEBUG', 'a debug message'),
('INFO', 'something info'),
)
For full control, pass a single callable:
def extract(record):
return {'level': record['level'].name, 'message': record['message']}
with LogCapture(LoguruSource(extract)) as log:
logger.debug('a debug message')
logger.error('an error')
log.check(
{'level': 'DEBUG', 'message': 'a debug message'},
{'level': 'ERROR', 'message': 'an error'},
)
Filtering by level¶
To capture only messages at or above a minimum level:
with LogCapture(LoguruSource(level='WARNING')) as log:
logger.info('ignored')
logger.warning('captured')
log.check(('WARNING', 'captured'))
Combining with standard library logging¶
If your application uses both loguru and the standard library’s logging,
pass both a LoggingSource and a
LoguruSource to one LogCapture
to capture and check both:
import logging
from testfixtures import LogCapture, LoggingSource
from testfixtures.loguru import LoguruSource
with LogCapture(LoggingSource(), LoguruSource()) as log:
logging.warning('from standard library logging')
logger.warning('from loguru')
log.check(
('WARNING', 'from standard library logging'),
('WARNING', 'from loguru'),
)
Exceptions¶
When code logs an exception, the underlying exception object is stored in
Entry’s exception attribute:
from testfixtures import LogCapture, compare
from testfixtures.loguru import LoguruSource
with LogCapture(LoguruSource()) as logs:
try:
raise ValueError('boom')
except ValueError:
logger.exception('oh no')
compare(logs.entries[-1].exception, expected=ValueError('boom'))
Checking the configuration of your log sinks¶
LoguruSource is good for checking that your code is logging
the correct messages; just as important is checking that your application has correctly
configured loguru sinks. If you have a setup_logging function such as this:
import sys
from loguru import logger
def setup_logging(level: str = 'INFO') -> None:
logger.remove()
logger.add(sys.stderr, level=level, format='{level} {message}')
This can be tested with a unit test such as the following:
import logging
import sys
from loguru._handler import Handler as LoguruHandler
from loguru._simple_sinks import StreamSink
from testfixtures import Replacer, compare, like
def test_setup_logging() -> None:
with Replacer() as replace:
replace(logger._core.handlers, {}, container=logger._core, name='handlers')
setup_logging(level='WARNING')
handlers = list(logger._core.handlers.values())
compare(
handlers,
expected=[
like(
LoguruHandler,
levelno=logging.WARNING,
_sink=like(StreamSink, _stream=sys.stderr),
)
],
)
compare(handlers[0]._formatter.strip(), expected='{level} {message}\n{exception}')