Search code examples
unit-testingloggingjmockit

How to use dynamic partial mocking with JMockit and Logger interface?


I've been working with JMockit and its admittedly steep learning curve. I'm pretty new with mocking in general so please excuse the ignorance.

I'm trying to mock out the Logger interface so that I can verify the catch statement is working correctly. Call this an exercise in understanding how JMockit works. The implementing class for the Logger interface is Log4jLoggerAdapter so I thought if I passed an instance of that into my Expectations() block, JMockit would use dynamic partial mocking and "see" my logger statement. Instead, I get the following error:

mockit.internal.MissingInvocation: Missing invocation of: org.slf4j.impl.Log4jLoggerAdapter#error(String msg, Throwable t)

The Class Being Tested

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

  ... // Other variables

  @Override
  public void connect() {
      String info = getServiceInfo();
      try {
        connector = MyConnectionFactory.connect(info);
      } catch (Exception e) {
        LOGGER.error("Exception connecting to your service with: " + info, e);
      }
  }

  ...  // Other methods
}

My @Test

public class MyLoggedClassTest {

@Tested
MyLoggedClass myLoggedClass;

@Test
public void myLoggingTest(@Mocked final Log4jLoggerAdapter logger){

    new Expectations(MyConnectionFactory.class, logger){{
      MyConnectionFactory.connect(anyString);
      result = new Exception();

      logger.error(anyString, (Throwable)any);
    }};

    myLoggedClass.connect();
}

I'd detail the other approaches I've tried but this page would turn into a book. This is my best approach. Any ideas?

* Update * (yes, that was quick)

I changed @Mocked to @Cascading and removed the logger field from my Expectations signature and it worked. I don't understand why. Can someone please provide an explanation? Fumbling about until you stumble on something that works but you don't understand is not a recipe for success. See below:

@Test
public void myLoggingTest(@Cascading final Log4jLoggerAdapter logger){

    new Expectations(MyConnectionFactory.class){{
      MyConnectionFactory.connect(anyString);
      result = new Exception();

      logger.error(anyString, (Throwable)any);
    }};

    myLoggedClass.connect();
}

Solution

  • No need for partial mocking in this case, just mock MyConnectionFactory in the usual way. The only tricky part is how to mock the class that implements the Logger interface, considering that it's instantiated from a static class initializer. As it happens, there is a feature in the mocking API for that (using JMockit 1.14):

    public class MyLoggedClassTest
    {
        @Tested MyLoggedClass myLoggedClass;
    
        @Test
        public void myLoggingTest(
            @Mocked MyConnectionFactory conFac, 
            @Capturing final Logger logger)
        {
            new Expectations() {{
                MyConnectionFactory.connect(anyString);
                result = new Exception();
            }};
    
            myLoggedClass.connect();
    
            new Verifications() {{
                logger.error(anyString, (Throwable)any);
            }};
        }
    }
    

    With a @Capturing mocked type, any implementation class will get mocked, so the test doesn't need to know about Log4jLoggerAdapter.