Rrr
Rrr

Reputation: 1777

Should we unit test logging?

It's usual to see logging functionality in the code:

public class A {

    private static final Log LOG = LogFactory.getLog(A.class);

and usage:

} catch (Exception e) {
    LOG.error(e.getMessage(), e);
    throw e;
}

but I never saw even single unit test for such code.

Off course I do test throwing exception and exception type, but should I write test for checking logging information? I tend to think that logging is another part of system behavior, so it's quit logically to cover it in the tests.

Assuming that I should cover it, means that I should change my original code to inject mock log and check that "error" method was invoked with expected message. But what to do if my original class is service and it's instantiated by spring, should I inject some logger as well as other dependencies?

Upvotes: 40

Views: 43742

Answers (7)

vvauban
vvauban

Reputation: 485

I used the Baeldung website tip: https://www.baeldung.com/junit-asserting-logs

1️⃣ You have a class creating logs:

    public class BusinessWorker {
    private static Logger LOGGER = LoggerFactory.getLogger(BusinessWorker.class);

    public void generateLogs(String msg) {
        LOGGER.trace(msg);
        LOGGER.debug(msg);
        LOGGER.info(msg);
        LOGGER.warn(msg);
        LOGGER.error(msg);
    }
}

2️⃣ You use copy-verbatim another class stacking the logs temporarily:

public class MemoryAppender extends ListAppender<ILoggingEvent> {
    public void reset() {
        this.list.clear();
    }

    public boolean contains(String string, Level level) {
        return this.list.stream()
          .anyMatch(event -> event.toString().contains(string)
            && event.getLevel().equals(level));
    }

    public int countEventsForLogger(String loggerName) {
        return (int) this.list.stream()
          .filter(event -> event.getLoggerName().contains(loggerName))
          .count();
    }

    public List<ILoggingEvent> search(String string) {
        return this.list.stream()
          .filter(event -> event.toString().contains(string))
          .collect(Collectors.toList());
    }

    public List<ILoggingEvent> search(String string, Level level) {
        return this.list.stream()
          .filter(event -> event.toString().contains(string)
            && event.getLevel().equals(level))
          .collect(Collectors.toList());
    }

    public int getSize() {
        return this.list.size();
    }

    public List<ILoggingEvent> getLoggedEvents() {
        return Collections.unmodifiableList(this.list);
    }
}

3️⃣ You use it in your unit test

@Before
public void setup() {
    Logger logger = (Logger) LoggerFactory.getLogger(LOGGER_NAME);
    memoryAppender = new MemoryAppender();
    memoryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
    logger.setLevel(Level.DEBUG);
    logger.addAppender(memoryAppender);
    memoryAppender.start();
}

@Test
public void test() {
    BusinessWorker worker = new BusinessWorker();
    worker.generateLogs(MSG);
        
    assertThat(memoryAppender.countEventsForLogger(LOGGER_NAME)).isEqualTo(4);
    assertThat(memoryAppender.search(MSG, Level.INFO).size()).isEqualTo(1);
    assertThat(memoryAppender.contains(MSG, Level.TRACE)).isFalse();
}

Upvotes: 0

Mike
Mike

Reputation: 2605

I would definitely consider unit tests for logging scenarios. when testing, think about the information you would require in a situation where the code has failed. if you have a live issue you'd want to be reassured that you have enough information to find the cause of the issue.

avoid excessive logging as you may not see the wood through the trees when debugging.

Upvotes: 1

kap
kap

Reputation: 1804

Yes, we should test logging when the logging is doing something that is required. For example, you have hooks in some external application that scans the log for certain events. In that case you certainly want to ensure the logging is done.

Of course you do not want to test every loging event, and I would think that mostly only ERROR (and not all of them) should be tested.

With modern logging frameworks such as SLF4j you can simply inject a custom handler that stores the events for in memory and that can be asserted against afterwards.

There are two of them that come to my mind right now:

SLF4JTesting: Requires no modification of logging configuration but requires to inject a logging factory which might lead to modified code.

SLF4J Test: Not as powerful as slf4jtesting and seems not to be developed, but works well with existing code. No modifications besides the logger configuration for test.

When using SLF4J Test, the assertions are quite strict and check the whole event for equality. A custom matcher is probably interesting in such a case:

public static Matcher<LoggingEvent> errorMessageContains(final String s) {
    return new TypeSafeMatcher<LoggingEvent>() {
        @Override
        public void describeTo(final Description description) {
            description.appendText(" type " + Level.ERROR + " should contain ")
                    .appendValue(s);
        }

        @Override
        protected void describeMismatchSafely(final LoggingEvent item, final Description mismatchDescription) {
            mismatchDescription.appendText(" was type ").appendValue(l)
                    .appendText(" message ").appendValue(item.getMessage());
        }

        @Override
        protected boolean matchesSafely(final LoggingEvent item) {
            return item.getLevel().equals(Level.ERROR)
                    && item.getMessage().contains(s);
        }
    };
}

This only checks that the message contains a text but not if it is equal. Thus, when the message is modified to fix a typo or give more detail, the test does not break if the essential part is still contained.

Upvotes: 9

Ming Cheng
Ming Cheng

Reputation: 119

there's another way: you can mock LogFactory! for example:

import junit.framework.Assert;
import mockit.Mock;
import mockit.MockUp;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;

public class XXXTest {
    class MyLog implements Log {
        public static final String INFO = "info";

        private String logLevel;
        private Object logContent;

        public String getLogLevel() {
            return logLevel;
        }

        public Object getLogContent() {
            return logContent;
        }

        @Override
        public void info(Object o) {
            logLevel = "info";
            logContent = o;
        }

        //Implement other methods
    }

    @Test
    public void testXXXFunction() {
        final MyLog log = new MyLog();
        new MockUp<LogFactory>() {
            @Mock
            public Log getLog(String name) {
                return log;
            }
        };

        //invoke function and log by MyLog
        FunctionToBeTest.invoke();
        Assert.assertEquals("expected log text", log.getLogContent());
    }
}

good luck!

Upvotes: 1

ssmith
ssmith

Reputation: 8962

If the logging is a business requirement, and will provide business value (i.e. in the event of a failure, to diagnose or triage a problem), then you should treat it as any other requirement. As such, you should probably write unit tests not to verify that your logging library works, but to verify that, under the expected circumstances, your code logs what it should.

More on this topic: https://ardalis.com/logging-and-monitoring-are-requirements

Upvotes: 1

Carl Raymond
Carl Raymond

Reputation: 4479

It's not up to you to test the logging library. But it can be worthwhile to test that when an exception is thrown, your class logs a message at the right level. What you're testing is that your code does the right thing with the logging library.

To make the code above testable, use dependency injection. This assumes that the logger implements an interface, ILog. You would pass in the logger as a constructor parameter to class A. Then the test code would create a mock implementation of ILog, and pass that into the constructor. Not shown in the code above is how the exception comes about, but presumably it would be through some other dependent object. So you mock that as well, and make it throw an exception. Then check that the mock ILog invoked the error method. Maybe you want to examine the message that it logs, but that might be going too far, by making the test code fragile.

Upvotes: 20

Matt Ball
Matt Ball

Reputation: 359986

I wouldn't unit test code that does nothing but call into a library that you trust.
Do you trust your logging library? If the test fails, is it because there's a bug in the library, or simply because you haven't configured the library correctly? Do you care about testing the configuration?

Upvotes: -1

Related Questions