Search code examples
javaloggingjunit

Java Unit test error: class org.slf4j.helpers.SubstituteLogger cannot be cast to class ch.qos.logback.classic.Logger


I am running unit tests and sometimes getting this error message below. How can I poll Logger to prevent bad random tests?

Someone mentioned comment in this answer link, https://stackoverflow.com/a/51812144/15435022

Comment: Plus note that LoggerFactory.getLogger() might return an org.slf4j.helpers.SubstituteLogger instance despite the usage of Logback while the logging is still initializing. You might need to poll for ch.qos.logback.classic.Logger up to a couple of milliseconds if you don't want flaky tests :)

import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;

@Test
public void testData() 
    Logger logger = (Logger) LoggerFactory.getLogger(ProfileEventHandler.class);
    ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
    listAppender.start();
    logger.addAppender(listAppender);

Error Returned:

java.lang.ClassCastException: class org.slf4j.helpers.SubstituteLogger cannot be cast to class ch.qos.logback.classic.Logger (org.slf4j.helpers.SubstituteLogger and ch.qos.logback.classic.Logger are in unnamed module of loader 'app')

Note: This answer did not resolve issue, since its not using list appender org.slf4j.helpers.SubstituteLogger cannot be cast to ch.qos.logback.classic.Logger


Solution

  • If I understand correctly what that commentor meant and how SubstituteLogger works during and after initialization, then the most straightforward approach would be to loop a set number of times, with a delay between each iteration, and check the type of the logger. If the logger is the correct type, return it. Otherwise, if you exhaust the loop, throw an exception to fail the test.

    Here's an example. Not sure which unit test framework you're using, but the example assumes JUnit 5.

    import static org.junit.jupiter.api.Assertions.fail;
    
    import java.time.Duration;
    import org.junit.jupiter.api.Test;
    import org.slf4j.LoggerFactory;
    
    class FooTests {
    
      private static final int LOGBACK_POLL_ATTEMPTS = 5;
      private static final Duration LOGBACK_POLL_DELAY = Duration.ofMillis(10);
      
      private static ch.qos.logback.classic.Logger getLogbackLogger(Class<?> cl) {
        try {
          org.slf4j.Logger slf4jLogger = null;
          for (int i = 0; i < LOGBACK_POLL_ATTEMPTS; i++) {
            slf4jLogger = LoggerFactory.getLogger(cl);
            if (slf4jLogger instanceof ch.qos.logback.classic.Logger logbackLogger) {
              return logbackLogger;
            }
            // 'sleep(Duration)' added in Java 19; use 'sleep(long)' if needed
            Thread.sleep(LOGBACK_POLL_DELAY);
          }
          fail("SLF4J never returned a Logback logger. Last returned = " + slf4jLogger);
        } catch (InterruptedException ex) {
          fail("Thread interrupted while polling for Logback logger", ex);
        }
        throw new Error("unreachable code"); // satisfy compiler
      }
    
      @Test
      void testData() {
        var logger = getLogbackLogger(ProfileEventHandler.class);
        // rest of test
      }
    }
    

    Whether or not Assertions::fail is the correct way to fail the test is up to you. Perhaps it would be better to use Assumptions::abort or to directly throw a different exception. Or maybe a combination thereof.