Search code examples
javaunit-testingmockitojunit5

JUnit5/Mockito - test coverage on a void method that throws and handles an exception


I have a service class MyService with following method

    private LoadingCache<String, Integer> attemptsCache;

    public MyService() {
       super();
       attemptCache = CacheBuilder.newBuilder()
           .expireAfterWrite(1, TimeUnits.HOURS)
           .build(new CacheLoader<String, Integer>() {
              @Override
              public Integer load(String key) throws Exception {
                  return 0
              }
       });
    }

    public void unlockFailed(String key) {

        int attempts = 0;

        try {
            attempts = attemptsCache.get(key);
        }catch (ExecutionException e) {
            attempts = 0; //unit test is not covering this block
        }

        attempts++;
        attemptsCache.put(key, attempts);
    }

My existing tests are passing and providing coverage for this method in all except for the catch block.

I would like to unit test this method using JUnit5, Mockito in order to get the coverage of the catch block but I dont know how to make a unit test that will provide coverage for the above catch block.

I have tried few things and most I can do is this:


private final String USER = "fakeuser";
@Spy
@InjectMocks
private UnlockAttemptServiceImpl sut;
    

@DisplayName("unlockFailed should handle ExecutionException")
@Test()
public void unlockFailed_should_handle_ExecutionException() throws ExecutionException {

    // Arrange
    LoadingCache<String, Integer> attemptsCache = Mockito.mock(LoadingCache.class);
    doThrow(new ExecutionException("Dummy ExecutionException", null)).when(attemptsCache).get(USER);

    // Act
    sut.unlockFailed(USER);

    // Assert
    ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () -> {
        // Act
        attemptsCache.get(USER);
    });

    Mockito.verify(attemptsCache, times(1)).get(USER);
    Mockito.verify(sut, times(1)).unlockFailed(USER);
    Assertions.assertEquals(exception.getMessage(), "Dummy ExecutionException");
}

However, while the above test will pass, it will not provide coverage for the catch block in unlockFailed() method.


Solution

  • Inject a factory to create your cache or wrap it in a custom class.

    Factory

    interface CacheFactory {
      LoadingCache<String, Integer> createCache();
    }
    
    class ExpiringCacheFactory implements CacheFactory {
      LoadingCache<String, Integer> createCache() {
          return CacheBuilder.newBuilder()
               .expireAfterWrite(1, TimeUnits.HOURS)
               .build(new CacheLoader<String, Integer>() {
                  @Override
                  public Integer load(String key) throws Exception {
                      return 0
                  }
           });
      }
    }
    
    class MyService
        private LoadingCache<String, Integer> attemptsCache;
    
        public MyService(final CacheFactory cacheFactory) {
           super();
           attemptCache = cacheFactory.createCache();
        }
    }
    

    In your production code:

    final MyService myService = new MyService(new ExpiringCacheFactory());
    

    In your test:

    LoadingCache<String, Integer> attemptsCacheMock = Mockito.mock(LoadingCache.class);
    doThrow(new ExecutionException("Dummy ExecutionException", null)).when(attemptsCacheMock).get(USER);
    final MyService sut = new MyService(() -> attemptsCacheMock);
    

    Custom wrapper class

    interface MyLoadingCache {
      // methods from (Loading)Cache that you want to expose
    }
    
    class MyLoadingCacheImpl implements MyLoadingCache {
      private LoadingCache<String, Integer> attemptsCache;
    
      public MyLoadingCacheImpl() {
          this.attemptsCache = CacheBuilder.newBuilder()
               .expireAfterWrite(1, TimeUnits.HOURS)
               .build(new CacheLoader<String, Integer>() {
                  @Override
                  public Integer load(String key) throws Exception {
                      return 0
                  }
           });
      }
    }
    
    class MyService
        private MyLoadingCache attemptsCache;
    
        public MyService(final MyLoadingCache attemptsCache) {
           super();
           this.attemptCache = attemptsCache;
        }
    }
    

    In your production code:

    final MyService myService = new MyService(new MyLoadingCacheImpl());
    

    In your test:

    MyLoadingCache cacheMock = Mockito.mock(MyLoadingCache.class);
    doThrow(new ExecutionException("Dummy ExecutionException", null)).when(cacheMock).get(USER);
    final MyService sut = new MyService(cacheMock);
    

    But you might as well inject the LoadingCache directly.


    Similar solutions are discussed in the answer to "Why is my class not calling my mocked methods in unit test?"