Mark
Mark

Reputation: 2158

How can I unit test my log messages using testng

We use testng as out testing framework. We also use Lombok @Log4j2 to instantiate our log objects. I need to test some code that it logs certain messages under certain conditions.

I have seen examples using junit and Mockito. But I cannot find how to do it in testng. Switching to junit is not an option.

Edit

I have implemented a class (CaptureLogger) which extends AbstractLogger

import org.apache.logging.log4j.spi.AbstractLogger;

public class CaptureLogger extends AbstractLogger {
    ...
}

I am unable to to hook it up to the logger for the class under test.

CaptureLogger customLogger = (CaptureLogger) LogManager.getLogger(MyClassUnderTest.class);

generates an error message:

java.lang.ClassCastException: org.apache.logging.log4j.core.Logger cannot be cast to CaptureLogger

I have found out that LogManager.getLogger returns the Logger interface, not the Logger object (which implements the Logger interface).

How can I create an instance of my CaptureLogger?

Upvotes: 0

Views: 5263

Answers (2)

ptanov
ptanov

Reputation: 76

You can define your own appender like this:

package com.xyz;

import static java.util.Collections.synchronizedList;

import java.util.ArrayList;
import java.util.List;

import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;

@Plugin(name = "LogsToListAppender", category = "Core", elementType = Appender.ELEMENT_TYPE)
public class LogsToListAppender extends AbstractAppender {

    private static final List<LogEvent> events = synchronizedList(new ArrayList<>());

    protected LogsToListAppender(String name, Filter filter) {
        super(name, filter, null);
    }

    @PluginFactory
    public static LogsToListAppender createAppender(@PluginAttribute("name") String name,
            @PluginElement("Filter") Filter filter) {
        return new LogsToListAppender(name, filter);
    }

    @Override
    public void append(LogEvent event) {
        events.add(event);
    }

    public static List<LogEvent> getEvents() {
        return events;
    }
}

Then create a file called log4j2-logstolist.xml in the root of the classpath where the appender will be referenced:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages="com.xyz" >
    <Appenders>
        <LogsToListAppender name="LogsToListAppender" />
    </Appenders>

    <Loggers>
        <Root level="TRACE">
            <AppenderRef ref="LogsToListAppender" />
        </Root>
    </Loggers>
</Configuration>

You should take special care (to update it properly) of the attribute packages="com.xyz" (the package of your appender) or it won't be available. For more information check https://www.baeldung.com/log4j2-custom-appender

And finally create TestNG test:

package com.xyz;

import static org.testng.Assert.assertTrue;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.config.Configurator;
import org.testng.annotations.Test;

@Test
public class LogsTest {

    static {
        Configurator.initialize(null, "classpath:log4j2-logstolist.xml");
    }

    @Test
    public void testLogs() {
        // call your code that produces log, e.g.
        LogManager.getLogger(LogsTest.class).trace("Hello");
        assertTrue(LogsToListAppender.getEvents().size() > 0);
    }
}

As you can see we are forcing Log4j2 to use the custom configuration with Configurator.initialize(null, "classpath:log4j2-logstolist.xml"); when the class is initialized (static{} block).

Keep in mind that it will be useful for you to check logger name as well, e.g. LogsToListAppender.getEvents().stream().filter(a -> CLASS_THAT_PRODUCES_LOG.class.getName().equals(a.getLoggerName())).collect(toList());

you can access the actual message using LogEvent::getMessage() method

Upvotes: 1

Mark Bramnik
Mark Bramnik

Reputation: 42551

As Long as you're using Lombok for logger generation you can't do much at the level of the source code itself with the given tools. For example, if you place @Log4j2 annotation, it generates:

private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);

The compiled code already comes with this line.

You can try to mock LogManager.getLogger method with PowerMockito but I don't really like this kind of tools. Stating this though since it can be a viable direction.

There are couple of ways to work with the framework itself.

One way (and I'm not familiar with Log4j2 specifically but it should offer this capability - I did something similar with Log4j 1.x many years ago) is to provide your own implementation of logger and associate it with the logger factory at the level of Log4j2 configurations. Now if you do this, then the code generated by lombok will return your instance of logger that can memorize the messages that were logged at different levels (it's the custom logic you'll have to implement at the level of Logger).

Then the logger will have a method public List<String> getResults() and you'll call the following code during the verification phase:

   public void test() {
     UnderTest objectUnderTest = ...
     //test test test

     // verification
     MyCustomLogger logger = (MyCutomLogger)LogManager.getLogger(UnderTest.class);

     List<String> results =  logger.getResults();
     assertThat(results, contains("My Log Message with Params I expect or whatever");

   }

Another somewhat similar way I can think of is to create a custom appender that will memorize all the messages that were sent during the test. Then you could (declaratively or programmatically bind that appender to the Logger obtained by the LogFactory.getLogger for the class under test (or also for other classes depending on your actual needs).

Then let the test work and when it comes to verification - get the reference to the appender from the log4j2 system and ask for results with some public List<String> getResults() method what must exist on the appender in addition to the methods that it must implement in order to obey the Appender contract.

So the test could look something like this:

public void test () {
     MyTestAppender app = createMemorizingAppender();
     associateAppenderWithLoggerUnderTest(app, UnderTest.class);
     UnderTest underTest = ...

     // do your tests that involve logging operations

     // now the verification phase:

     List<String> results =  app.getResults();
     assertThat(results, contains("My Log Message with Params I expect or whatever");


}

Upvotes: 1

Related Questions