Search code examples
javaunit-testingjunitslf4jjmockit

Mocking and verifying SLF4J with JMockit


I have a class with SLF4J logger instanced like:

 public class MyClass {

    private static final Logger log = LoggerFactory.getLogger(MyClass.class);

    public void foo() {
      log.warn("My warn");
    }
  }

And I need to test it with JMockit like:

 @Test
 public void shouldLogWarn(@Mocked Logger log) throws Exception {
   new Expectations() {{
     log.warn(anyString);
   }};
   MyClass my = new MyClass();
   my.foo(); 
 }

After searching a lot I figured out, I need to use MockUp somehow. But can't get it how exactly.

Btw, I'm using last version of JMockit(1.29) where you no more can setField(log) for final static fields.


Solution

  • JMockit has the @Capturing annotation that works for this situation

    Indicates a mock field or a mock parameter for which all classes extending/implementing the mocked type will also get mocked.

    Future instances of a capturing mocked type (ie, instances created sometime later during the test) will become associated with the mock field/parameter. When recording or verifying expectations on the mock field/parameter, these associated instances are regarded as equivalent to the original mocked instance created for the mock field/parameter.

    This means that if you annotate it with @Capturing instead of @Mocked, every Logger that is created during the test run will be associated with one you annotated. So the following works:

     @Test
     public void shouldLogWarn(@Capturing final Logger logger) throws Exception {
       // This really ought to be a Verifications block instead
       new Expectations() {{
         logger.warn(anyString);
       }};
       MyClass my = new MyClass();
       my.foo(); 
     }
    

    As a side note, if all you want to do is verify that a method is called, it's better to use Verifications instead, since that is what it is intended for. So your code would look like this:

     @Test
     public void shouldLogWarn(@Capturing final Logger logger) throws Exception {
       MyClass my = new MyClass();
       my.foo(); 
       new Verifications() {{
         logger.warn(anyString);
       }};
     }
    

    Alternatively, you can use @Mocked on both Logger and LoggerFactory

    In some cases, @Capturing won't work as intended due to intricacies of how the annotation works. Fortunately, you can also get the same effect by using @Mocked on both Logger and LoggerFactory like so:

     @Test
     public void shouldLogWarn(@Mocked final LoggerFactory loggerFactory, @Mocked final Logger logger) throws Exception {
       MyClass my = new MyClass();
       my.foo(); 
       new Verifications() {{
         logger.warn(anyString);
       }};
     }
    

    Note: JMockit 1.34 through 1.38 has a bug that prevents this from working with slf4j-log4j12, and possibly other dependencies of SLF4J. Upgrade to 1.39 or later if you run into this bug.