Log Message Validation with JUnit 5


Introduction

In your unittests it is important to check whether the code under test has been executed correctly. Sometimes, all you can check is log statements. In this guide, we will leverage the JUnit 5 Extension Model to intercept and verify logging. We will asume Logback is used in the application.

Capture logging statements

The first thing we need is something to capture the log statements. Let’s call this LogCapture. We need to store the log statements so we can retrieve them later. Logback provides a ListAppender for this.

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.read.ListAppender;

import java.util.List;
import java.util.stream.Collectors;

/**
* Capture log statements
**/
public class LogCapture {

    private ListAppender<ILoggingEvent> listAppender = new ListAppender<>();

    /**
    * Default constructor
    **/
    LogCapture() {
    }

    /**
    * The the logging event at the specified index
    **/
    public LoggingEvent getLoggingEventAt(int index) {
        return (LoggingEvent) listAppender.list.get(index);
    }

    /**
    * Get the formatted message of logging event at the specified index.
    **/
    public String getFormattedMessageAt(int index) {
        return getLoggingEventAt(index).getFormattedMessage();
    }

    /**
    * Get a list of all LoggingEvents (the implementation, not the interface)
    **/
    public List<LoggingEvent> getLoggingEvents() {
        return listAppender.list.stream().map(e -> (LoggingEvent) e).collect(Collectors.toList());
    }

    /**
    * Remove all logging events
    **/
    public void clear() {
        listAppender.list.clear();
    }

    /**
    * Start capturing logging events
    **/
    void start() {
        listAppender.start();
    }

    /**
    * Stop capturing logging events, and clear the buffer
    **/
    void stop() {
        if (listAppender == null) {
            return;
        }

        listAppender.stop();
        clear();
    }

    /**
    * Return the list of logging events.
    **/
    ListAppender<ILoggingEvent> getListAppender() {
        return listAppender;
    }
}

Create the extension

Now we’re ready to create the extension. There are several things that need to be done. First, we want to include the LogCapture only in tests that need it. We do that by supplying an instance of LogCapture as a parameter. Next, we need to configure Logback to include our LogCapture for this specific test. And last, we need to clean up when the test is done.

ParameterResolver

We want to inject a LogCapture object into our test method. To do this, we need to implement the ParameterResolver interface. This interface has two methods: supportsParameter and resolveParameter. The first method checks whether this ParameterResolver can supply an Object of the requested type, and the second method supplies an instance of the parameter to be used.

import ch.qos.logback.classic.Logger;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.slf4j.LoggerFactory;

/**
* Extends the capabilities of your unittest to capture logging events.
**/
public class LogCaptureExtension implements ParameterResolver {

    private final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);

    private LogCapture logCapture;

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return parameterContext.getParameter().getType() == LogCapture.class;
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        logCapture = new LogCapture();

        setup();

        return logCapture;
    }

    private void setup() {
        logger.addAppender(logCapture.getListAppender());
        logCapture.start();
    }
}

AfterTestExecutionCallback

When the test is done, we need to clean up to provide a predictable environment for our tests. JUnit Jupiter provides lifecycle callbacks which we can use to alter the state of the extension before and after tests. We need to implement the AfterTestExecutionCallback interface, which will be called right after the test is completed.

import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
// omitted

public class LogCaptureExtension implements ParameterResolver, AfterTestExecutionCallback {

    // omitted

    @Override
    public void afterTestExecution(ExtensionContext context) {
        teardown();
    }

    private void teardown() {
        if (logCapture == null || logger == null) {
            return;
        }

        logger.detachAndStopAllAppenders();
        logCapture.stop();
    }
}

Use the extension

To use the extension we need to register it. There are several ways to register the extension. In our case, we can use the declarative approach by annotating our test class with @ExtendsWith(LogCaptureExtension.class). Now we can pass a LogCapture argument to our test and validate logging statements.

@ExtendWith(LogCaptureExtension.class)
public class MyTest {
    
    @Test
    void testMyMethod(LogCapture capture) {
        // test some stuff
        
        assertThat(capture.getFormattedMessageAt(0)).contains("Hello World!");
    }
}

Conclusion

In the realm of unit testing, ensuring that your code behaves as intended is paramount. Sometimes, the only insight you have into your code’s execution is through its log messages. This blog post has illuminated a powerful technique for asserting the correctness of these log messages within your unit tests.

By harnessing the JUnit 5 Extension Model, you can intercept and validate log messages efficiently. Assuming your application uses Logback, this guide has provided you with the tools and knowledge needed to implement this approach effectively.

To summarize the key takeaways:

Capture Logging Statements: You’ve learned how to create a LogCapture utility that captures log statements using Logback’s ListAppender. This utility stores log messages for subsequent validation.

Creating the Extension: The blog post introduced the LogCaptureExtension, which seamlessly integrates with your tests. It allows you to inject a LogCapture object into your test methods, making log message validation a breeze.

ParameterResolver and AfterTestExecutionCallback: Understanding the ParameterResolver interface and AfterTestExecutionCallback interface has enabled you to set up and tear down the log capture mechanism for your tests gracefully.

Using the Extension: By annotating your test classes with @ExtendWith(LogCaptureExtension.class), you’ve unlocked the power to pass a LogCapture argument to your tests, empowering you to scrutinize log messages with ease.

Incorporating log message testing into your unit tests provides an additional layer of assurance that your code is running correctly. This practice not only enhances the quality and reliability of your tests but also fosters a deeper understanding of your application’s behavior, ultimately leading to more robust and maintainable software. 


Geef een reactie

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *