javaspring-bootjunit-jupiter

Prevent exception from being logged in test


I have a SpringBoot test which asserts an exception is thrown for certain situations from the method tested. However the method tested catches and groups multiple errors, logs the details and (re) throws just one 'ServiceException' instead.
(log and rethrow the exact same exception would be an antipattern, this is not such case)
It is a service method which does much stuff and the user/client should not be bothered with all the details. Most of the issues would be irrelevant and there's nothing to do except maybe "try again later".

The test works correctly (passes when the exception is thrown) but I also see the original stacktrace logged (as it is supposed to when in production). However when doing tests, it is undesired to see this error show in logs as if it would be a real error. (Though could be a case for a test which is done poorly)

So the question is, how can I suppress the error from being logged just for this one test case? (Preventing the logging to happen for all tests is not a solution. Exception would be needed just for a specific test case)

Example of the method to test:

public boolean isThisParameterGoodToUse(Object parameter) throws ServiceException {
    try {
        boolean allWasOk = true;
        // Do stuff that may throw exceptions regardless of the parameter
        return allWasOk;
    } catch (IOException | HttpException | SomeException | YetAnotherException e) {
        String msg = "There was a problem you can do nothing about, except maybe 'try again later'.";
        this.log.error(msg, e); // Relevent for system monitors, nothing for the client
        throw new ServiceException(msg);
    }
}

And then the test would look something like this (Class is annotated with '@SpringBootTest' and it uses 'Jupiter-api'):

@Test
public void isThisParameterGoodToUse() {
    assertThrows(ServiceException.class,
        () -> this.injectedService.isThisParameterGoodToUse("This is not a good parameter at all!"));
}

And when I run the test, I get error message to log, e.g.:

com.myProd.services.SomeException: There was a problem you can do nothing about, except maybe 'try again later'.
    at ... <lots of stackTrace> ...

Solution

  • The solution is to look at this issue from another perspective. Instead of trying to suppress a log (error) message when you are excepting it, instead verify it is actually logged. This is somewhat the thing @knittl suggested in comments.

    In short. Provide a mock instance of the logger for the method you want to suppress the log messages at. (Depending on how you setup / use the class tested, you may need to ensure you have the mock logger set only for the certain tests)

    The example class below logs an error message or warning message depending on the situation. Normally we would be interested only about the outcome of the method (asserts True/False) as it is generally enough to verify things work. But if you test also the messages, you get the solution automatically.

    Example class (tested with SpringBoot, but the same logic works with other frameworks also)

    @Service
    public class MyService {
      private Logger log = LogManager.getLogger(MyService.class);
    
     /**
      * Protected so that this is available to test but no more than necessary.
      */
      protected void setLogger(Logger logger) {
        this.log = logger;
      }
    
      public boolean processData(String data) throws ServiceException {
        try {
          if (!this.validateDataContains(data, "MagicString")) {
            log.warn("The given data does not contain the MagicString!");
            return false;
          }
        } catch (Exception e) {
          log.error("The given data is bad!", e);
          throw new ServiceException("There was a problem you can do nothing about, except maybe 'try again later'.");
        }
        return true;
      }
    
      protected boolean validateDataContains(String data, String magicString) {
        if (data == null) {
          throw new NullPointerException("Data given was null");
        } else if (!data.contains(magicString)) {
          return false;
        }
        return true;
      }
    }
    

    And the test class

    @SpringBootTest
    public class MyServiceTest extends Assertions {
      @Autowired
      private MyService service;
      @Captor
      private ArgumentCaptor<String> stringCaptor;
    
      @Test
      public void logsErrorTest() {
        var mockLogger = Mockito.mock(Logger.class);
        service.setLogger(mockLogger);
    
        assertThrows(ServiceException.class,
          () -> this.injectedService.processData(null));
        
        Mockito.verify(mockLogger, Mockito.times(1)).error(stringCaptor.capture(), ArgumentMatchers.any(Throwable.class));
        assertEquals("The given data is bad!", stringCaptor.getValue());
      }
    
      @Test
      public void logsWarningTest() {
        var mockLogger = Mockito.mock(Logger.class);
        service.setLogger(mockLogger);
    
        assertFalse(service.processData("This is plain text"));
        
        Mockito.verify(mockLogger, Mockito.times(1)).warn(stringCaptor.capture());
        assertEquals("The given data does not contain the MagicString!", stringCaptor.getValue());
      }
    }
    

    If you are lazy, you don't have to verify the message logged, just provide a mock and forget the rest.