Whether you are building microservices or monoliths, accurate and robust logging is a critical element in any application. Logging can give insight on how an application is being used and provide developers and operations with detailed information on why an error occurred. It is important then, like any other feature in an application, that we use automated tests to verify that logging statements are accurate and do not cause errors themselves.
In this two part series we will look at some strategies for capturing and verifying logging statements. In part one we will look at a simple and easy to implement strategy for when you are executing your test suite sequentially. In part two we will focus on a couple of strategies for when you are executing your test suite in parallel.
For demonstrating how to capture and assert logging statements we have the very useful and accurately named
LogProducingService has a single method,
writeSomeLoggingStatements(), which produces a pair of logging statements. One statement written within the same thread as that of the calling test case and another statement written in a separate thread (this will be more important in part two).
Implementing a Custom Appender
In normal operation an application will typically be using either some form of a file appender to write statements to system logs, or in more recent trends, using an appender that sends statements to a message queue such as Kafka. There are some other options, but none are practical for verifying if the logging statements being produced are valid. So to handle this we will need to implement our own appender like this one:
In the above class I am extending AppenderBase and instead of printing the logging statements out to a file or sending them to a message queue I am adding them to a
List. To retrieve the logging statements for asserting in a test case there is
getEvents(), and to clear the list out between test cases I added
I named this class
StaticAppender because the list I am storing the logging statements in, as well as the methods to retrieve and clear that list, are all static. This is necessary as I want to be able to globally configure my test suite using
logback-test.xml; and the instance of
StaticAppender that logback creates won’t be easily available to my test code. By declaring the aforementioned fields and methods static, I have easy access to the logging statements my application is producing from within my test cases.
After implementing the appender, we need to configure logback to use it:
As mentioned above I am configuring logback for my test suite using
logback-test.xml and placed it in the
src/test/resources directory of the project. I did this because by order of precedence logback will load
logback-test.xmlfirst if it is present on classpath. So even though I have
logback.xml defined in my
src/main/resources, when executing my test suite I will be using
StaticAppender instead of
ConsoleAppender defined in
logback.xml. This is important as we don’t want to modify production code so that it is aware it is running in a test scenario versus actually deployed; that is a big no-no.
Finally the test case would look something like this:
I am a huge fan of AssertJ, which I am making use of here to quickly extract out the message field from a list of
ILoggingEvent and assert its value. If you want to learn more about using AssertJ in your automated testing, I wrote a blog article on that as well.
When executing a test suite in a single thread, as would often be the case, using a static appender should work fine, as long as you are cleaning up logging statements between test cases like I am doing in
clearLoggingStatements(). However if you have a large test suite and are wanting to execute tests in parallel to speed up your build times, then you will run into issues when using the
StaticAppender. This is because while multiple test cases are being executed simultaneously, all their logging statements are being written to a single
List. Your test cases won’t be able to know which log statements were written from the code it is executing from versus that of other test cases. There could create issues of logging statements being deleted when
clearEvents() is called mid test execution.
Luckily the issues around executing tests in parallel can be overcome with relative ease which I will cover in the next article in this series.
Capturing and verifying logging statements is actually a pretty simple task as seen in this article. No additional libraries, beyond your logging libraries, are needed, nor is complex configuration.
While testing logging output might never be the highest priority for a development shop, it is useful to know that testing logging output is easy and practical. Writing some tests for the logging statements your application is producing might save you some frustration next time you have a production issue and you are able to look through accurate and detailed logs as you track down the cause of the problem.
The code for this article can be found here: https://github.com/wkorando/assert-logging-statements