0

I'm getting the following error when run my tests together. If I run each test there is no issue.

Wanted but not invoked:
logger.error("Error message 2");
-> at acme.logger.MyCoreLogTest.errorWith(MyCoreLogTest.java:74)
Actually, there were zero interactions with this mock.

MyCoreLogger:

package acme.mycore.logger;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class MyCoreLog {
    private static final Logger logger = LogManager.getLogger("MyCoreLog");
    
    public static final String id = "my_id";
    
    public static void error(String message) {
        if (System.getenv(id) != null)
            logger.error(String.format("%s, %s=%s", message, id, System.getenv(id)));
        else
            logger.error(message);
    }
}

CoreLogTest:

package acme.mycore.logger;

import org.apache.logging.log4j.LogManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.apache.logging.log4j.Logger;
import org.mockito.MockedStatic;
import static org.mockito.Mockito.*;


class CoreLogTest {
    MockedStatic<LogManager> logManager;
    @BeforeEach
    public void setUp()
    {
        logManager = mockStatic(LogManager.class);
    }

    @AfterEach
    public void tearDown() throws Exception
    {
        logManager.close();
    }
    
    
    @Test
    void error() {
        String errorMessage = "error message";

        Logger logger = mock(Logger.class);
        logManager.when(() -> LogManager.getLogger("MyCoreLog")).thenReturn(logger);

        MyCoreLog.error(errorMessage);

        verify(logger).error(errorMessage); // Ok
    }
    
    @Test
    void errorWith() {
        String errorMessage = "error message 2";

        Logger logger = mock(Logger.class);
        logManager.when(() -> LogManager.getLogger("MyCoreLog")).thenReturn(logger);

        MyCoreLog.error(errorMessage);

        verify(logger).error(errorMessage);  // Wanted but not invoked: logger.error("error message 2"); Actually, there were zero interactions with this mock.
    }
}

Gabriel Muñumel
  • 1,876
  • 6
  • 34
  • 57

3 Answers3

2

The line

private static final Logger logger = LogManager.getLogger("MyCoreLog");

is executed and assigned only once, when your MyCoreLog class is initially loaded (on first access).

So in the first test "logger(1)" is assigned to the static field MyCoreLog#logger. The second test creates a new instance "logger(2)" which is never assigned to your static field (because static field initializers are executed once exactly). When executing the second test, your MyCoreLog still holds a reference to the logger from the first test.

This is another spin on Why are my mocked methods not called when executing a unit test? albeit a bit more tricky to detect. You should be able to verify by using a debugger and checking and comparing the object identity of the logger instances in both tests and in your class.

How to solve?

  1. Create only a single mock of "logger" and assign it in a @BeforeClass method or when loading your test class, then use this reference in verify and this verify only
  2. Get rid of the mocks and verify call altogether. Use reflection or your logging framework's facilities to query the list of active loggers and then perform the check on this logger instance.

Example code for solution 1:

class CoreLogTest {
    MockedStatic<LogManager> logManager;
    // only a single shared instance:
    private final Logger loggerMock = mock(Logger.class);

    @BeforeEach
    public void setUp()
    {
        logManager = mockStatic(LogManager.class);
        // LogManager.getLogger is still only executed by the first test!
        logManager.when(() -> LogManager.getLogger("MyCoreLog")).thenReturn(logger);
    }

    @AfterEach
    public void tearDown() throws Exception
    {
        logManager.close();
    }
    
    
    @Test
    void error() {
        String errorMessage = "error message";

        MyCoreLog.error(errorMessage);

        verify(logger).error(errorMessage); // Ok
    }
    
    @Test
    void errorWith() {
        String errorMessage = "error message 2";

        MyCoreLog.error(errorMessage);

        verify(logger).error(errorMessage);
    }
}
knittl
  • 246,190
  • 53
  • 318
  • 364
  • I tried your suggestions but unfortunately didn't work, however I think you reasoning is correct. At the end I pass `logger` as a parameter for my `error` method and works as expected. Thanks anyway. – Gabriel Muñumel May 09 '23 at 08:20
  • @GabrielMuñumel what does "didn't work" mean exactly? Still no calls? Did you verify if the verified instance is the instance from your service? – knittl May 09 '23 at 09:29
  • Indeed the verified instances are not the same. I see that in the `@BeforeEach` the `logger` instance is recreate even thought is marked as final. – Gabriel Muñumel May 09 '23 at 10:17
  • @GabrielMuñumel which logger instance in `@BeforeEach`? The logger instance is only created once in the fielde initializer. There cannot exist multiple instances of the `Logger loggerMock` field. – knittl May 09 '23 at 10:47
0

The only way I solved this was:

class CoreLogTest {
    private static MockedStatic<LogManager> logManager;
    // only a single shared instance:
    private static final Logger loggerMock = mock(Logger.class);

    @BeforeAll
    public static void setUp()
    {
        logManager = mockStatic(LogManager.class);
        // LogManager.getLogger is still only executed by the first test!
        logManager.when(() -> LogManager.getLogger("MyCoreLog")).thenReturn(loggerMock);
    }

    @AfterAll
    public static void tearDown()
    {
        logManager.close();
    }
    
    
    @Test
    void error() {
        String errorMessage = "error message";

        MyCoreLog.error(errorMessage);

        verify(logger).error(errorMessage); // Ok
    }
    
    @Test
    void errorWith() {
        String errorMessage = "error message 2";

        MyCoreLog.error(errorMessage);

        verify(logger).error(errorMessage);
    }
}
Gabriel Muñumel
  • 1,876
  • 6
  • 34
  • 57
0

I used the solution as posted by @knittl, with one small adjustment (adding static):

private static final Logger loggerMock = mock(Logger.class);

ps: couldn't add this as comment, due to low reputation

Annanraen
  • 21
  • 6