Unit Testing Your Logging¶
Validate Logging in Tests¶
Now that you’ve got some code emitting log messages (or even better, before you’ve written the code) you can write unit tests to verify it. Given good test coverage all code branches should already be covered by tests unrelated to logging. Logging can be considered just another aspect of testing those code branches. Rather than recreating all those tests as separate functions Eliot provides a decorator the allows adding logging assertions to existing tests.
Let’s unit test some code that relies on the LOG_USER_REGISTRATION
object we created earlier.
from myapp.logtypes import LOG_USER_REGISTRATION
class UserRegistration(object):
def __init__(self):
self.db = {}
def register(self, username, password, age):
self.db[username] = (password, age)
LOG_USER_REGISTRATION(
username=username, password=password, age=age).write()
Here’s how we’d test it:
from unittest import TestCase
from eliot import MemoryLogger
from eliot.testing import assertContainsFields, capture_logging
from myapp.registration import UserRegistration
from myapp.logtypes import LOG_USER_REGISTRATION
class LoggingTests(TestCase):
def assertRegistrationLogging(self, logger):
"""
Logging assertions for test_registration.
"""
self.assertEqual(len(logger.messages), 1)
msg = logger.messages[0]
assertContainsFields(self, msg,
{u"username": u"john",
u"password": u"password",
u"age": 12}))
@capture_logging(assertRegistrationLogging)
def test_registration(self, logger):
"""
Registration adds entries to the in-memory database.
"""
registry = UserRegistration()
registry.register(u"john", u"password", 12)
self.assertEqual(registry.db[u"john"], (u"passsword", 12))
Besides calling the given validation function the @capture_logging
decorator will also validate the logged messages after the test is done.
E.g. it will make sure they are JSON encodable.
Messages were created using ActionType
and MessageType
will be validated using the applicable Field
definitions.
You can also call MemoryLogger.validate
yourself to validate written messages.
If you don’t want any additional logging assertions you can decorate your test function using @capture_logging(None)
.
Testing Tracebacks¶
Tests decorated with @capture_logging
will fail if there are any tracebacks logged (using write_traceback
or writeFailure
) on the theory that these are unexpected errors indicating a bug.
If you expected a particular traceback to be logged you can call MemoryLogger.flush_tracebacks
, after which it will no longer cause a test failure.
The result will be a list of traceback message dictionaries for the particular exception.
from unittest import TestCase
from eliot.testing import capture_logging
class MyTests(TestCase):
def assertMythingBadPathLogging(self, logger):
messages = logger.flush_tracebacks(OSError)
self.assertEqual(len(messages), 1)
@capture_logging(assertMythingBadPathLogging)
def test_mythingBadPath(self, logger):
mything = MyThing()
# Trigger an error that will cause a OSError traceback to be logged:
self.assertFalse(mything.load("/nonexistent/path"))
Testing Message and Action Structure¶
Eliot provides utilities for making assertions about the structure of individual messages and actions.
The simplest method is using the assertHasMessage
utility function which asserts that a message of a given MessageType
has the given fields:
from eliot.testing import assertHasMessage, capture_logging
class LoggingTests(TestCase):
@capture_logging(assertHasMessage, LOG_USER_REGISTRATION,
{u"username": u"john",
u"password": u"password",
u"age": 12})
def test_registration(self, logger):
"""
Registration adds entries to the in-memory database.
"""
registry = UserRegistration()
registry.register(u"john", u"password", 12)
self.assertEqual(registry.db[u"john"], (u"passsword", 12))
assertHasMessage
returns the found message and can therefore be used within more complex assertions. assertHasAction
provides similar functionality for actions (see example below).
More generally, eliot.testing.LoggedAction
and eliot.testing.LoggedMessage
are utility classes to aid such testing.
LoggedMessage.of_type
lets you find all messages of a specific MessageType
.
A LoggedMessage
has an attribute message
which contains the logged message dictionary.
For example, we could rewrite the registration logging test above like so:
from eliot.testing import LoggedMessage, capture_logging
class LoggingTests(TestCase):
def assertRegistrationLogging(self, logger):
"""
Logging assertions for test_registration.
"""
logged = LoggedMessage.of_type(logger.messages, LOG_USER_REGISTRATION)[0]
assertContainsFields(self, logged.message,
{u"username": u"john",
u"password": u"password",
u"age": 12}))
@capture_logging(assertRegistrationLogging)
def test_registration(self, logger):
"""
Registration adds entries to the in-memory database.
"""
registry = UserRegistration()
registry.register(u"john", u"password", 12)
self.assertEqual(registry.db[u"john"], (u"passsword", 12))
Similarly, LoggedAction.of_type
finds all logged actions of a specific ActionType
.
A LoggedAction
instance has start_message
and end_message
containing the respective message dictionaries, and a children
attribute containing a list of child LoggedAction
and LoggedMessage
.
That is, a LoggedAction
knows about the messages logged within its context.
LoggedAction
also has a utility method descendants()
that returns an iterable of all its descendants.
We can thus assert that a particular message (or action) was logged within the context of another action.
For example, let’s say we have some code like this:
LOG_SEARCH = ActionType(...)
LOG_CHECK = MessageType(...)
class Search:
def search(self, servers, database, key):
with LOG_SEARCH(database=database, key=key):
for server in servers:
LOG_CHECK(server=server).write()
if server.check(database, key):
return True
return False
We want to assert that the LOG_CHECK message was written in the context of the LOG_SEARCH action. The test would look like this:
from eliot.testing import LoggedAction, LoggedMessage, capture_logging
import searcher
class LoggingTests(TestCase):
@capture_logging(None)
def test_logging(self, logger):
searcher = Search()
servers = [buildServer(), buildServer()]
searcher.search(servers, "users", "theuser")
action = LoggedAction.of_type(logger.messages, searcher.LOG_SEARCH)[0]
messages = LoggedMessage.of_type(logger.messages, searcher.LOG_CHECK)
# The action start message had the appropriate fields:
assertContainsFields(self, action.start_message,
{"database": "users", "key": "theuser"})
# Messages were logged in the context of the action
self.assertEqual(action.children, messages)
# Each message had the respective server set.
self.assertEqual(servers, [msg.message["server"] for msg in messages])
Or we can simplify further by using assertHasMessage
and assertHasAction
:
from eliot.testing import LoggedAction, LoggedMessage, capture_logging
import searcher
class LoggingTests(TestCase):
@capture_logging(None)
def test_logging(self, logger):
searcher = Search()
servers = [buildServer(), buildServer()]
searcher.search(servers, "users", "theuser")
action = assertHasAction(self, logger, searcher.LOG_SEARCH, succeeded=True,
startFields={"database": "users",
"key": "theuser"})
# Messages were logged in the context of the action
messages = LoggedMessage.of_type(logger.messages, searcher.LOG_CHECK)
self.assertEqual(action.children, messages)
# Each message had the respective server set.
self.assertEqual(servers, [msg.message["server"] for msg in messages])
Restricting Testing to Specific Messages¶
If you want to only look at certain messages when testing you can log to a specific eliot.Logger
object.
The messages will still be logged normally but you will be able to limit tests to only looking at those messages.
You can log messages to a specific Logger
:
from eliot import Message, Logger
class YourClass(object):
logger = Logger()
def run(self):
# Create a message with two fields, "key" and "value":
msg = Message.new(key=123, value=u"hello")
# Write the message:
msg.write(self.logger)
As well as actions:
from eliot import start_action
logger = Logger()
with start_action(logger, action_type=u"store_data"):
x = get_data()
store_data(x)
Or actions created from ActionType
:
from eliot import Logger
from myapp.logtypes import LOG_USER_REGISTRATION
class UserRegistration(object):
logger = Logger()
def __init__(self):
self.db = {}
def register(self, username, password, age):
self.db[username] = (password, age)
msg = LOG_USER_REGISTRATION(
username=username, password=password, age=age)
# Notice use of specific logger:
msg.write(self.logger)
The tests would then need to do two things:
- Decorate your test with
validate_logging
instead ofcapture_logging
. - Override the logger used by the logging code to use the one passed in to the test.
For example:
from eliot.testing import LoggedMessage, validate_logging
class LoggingTests(TestCase):
def assertRegistrationLogging(self, logger):
"""
Logging assertions for test_registration.
"""
logged = LoggedMessage.of_type(logger.messages, LOG_USER_REGISTRATION)[0]
assertContainsFields(self, logged.message,
{u"username": u"john",
u"password": u"password",
u"age": 12}))
# validate_logging only captures log messages logged to the MemoryLogger
# instance it passes to the test:
@validate_logging(assertRegistrationLogging)
def test_registration(self, logger):
"""
Registration adds entries to the in-memory database.
"""
registry = UserRegistration()
# Override logger with one used by test:
registry.logger = logger
registry.register(u"john", u"password", 12)
self.assertEqual(registry.db[u"john"], (u"password", 12))