2

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> ...
Ville Myrskyneva
  • 1,560
  • 3
  • 20
  • 35
  • You can't, unless you want to hack. Eg by putting a condition around the log.error to test if you are in a \@SpringBootTest – John Williams Feb 02 '23 at 14:04
  • 1
    Can you swap out the logger for this test and replace it with a dummy/no-op logger? – knittl Feb 02 '23 at 14:31
  • Maybe have a different logger configuration file in your resources subdirectory under the `src/test` directory tree? – k314159 Feb 02 '23 at 15:15
  • @knittl Yes I could do that, but I was hoping a solution in which I would not need to add custom code for tests into the class/method tested. – Ville Myrskyneva Feb 03 '23 at 06:09
  • @k314159 That would affect to the whole class tested. I would need it to be just for this specific test. Otherwise it would work. – Ville Myrskyneva Feb 03 '23 at 06:11
  • @JohnWilliams I am afraid you may be correct. The hack-condition is not an option, though I'm not giving up just yet finding a solution. – Ville Myrskyneva Feb 03 '23 at 07:45

3 Answers3

2

If logging should be suppressed for a single test-class you can use

@SpringBootTest(properties = "logging.level.path.to.service.MyService=OFF")

If logging should be suppressed in all your tests then add this to your application.properties

test/resources/application.properties

logging.level.path.to.service.MyService=OFF

UPDATE

Suppress logging for a single test could be done by nesting your test in a separate class

@SpringBootTest
class DemoServiceTest {

    @Autowired DemoService service;

    @Test
    void testWithErrorLogging() {
        // ...
    }

    @Nested
    @SpringBootTest(properties = {"logging.level.com.example.demo.DemoService=OFF"})
    class IgnoreExceptionTests{
        @Test
        void isThisParameterGoodToUseWithOutError() {
            Assertions.assertThrows(
                    ServiceException.class,
                    () -> {
                        service.isThisParameterGoodToUse("blabala");
                    }
            );
        }
    }
}

Dirk Deyne
  • 6,048
  • 1
  • 15
  • 32
  • Close to what I am looking for, though it would have too broad effect when cancelling all logging from the class. I would require this happen just for this one specific logging scenario with this one specific test. – Ville Myrskyneva Feb 03 '23 at 06:17
  • That update about nesting looks promising. I'll give it a go next time I end up with similar situation. Since I eventually ended up with a situation where it was desired to test the log output also, I marked that solution as accepted. – Ville Myrskyneva Jun 19 '23 at 04:59
0

Don't suppress the exception in logs, even in test.

Seeing exceptions thrown in tests is a good thing, since it means that your test covers a case in which they would be thrown.

The most desirable thing would be to validate that the exception along with the right message was thrown properly too (since you wouldn't want to mock the logger or spy on it or anything).

@Test
void isThisParameterGoodToUse() {
    assertThrows(ServiceException.class,
         () -> this.injectedService.isThisParameterGoodToUse("This is not a good parameter at all!"), 
   "There was a problem you can do nothing about, except maybe 'try again later'.");
}
Makoto
  • 104,088
  • 27
  • 192
  • 230
  • You are correct about this, though the test in question is not concerned about the exception been thrown. Instead it check's the code is able to continue on a particular error situation. The log is byproduct which I wasn't interested at all. Though this approach also made me look at the issue from wrong view which prevented from seeing the solution. (Posting solution as answer) – Ville Myrskyneva Jun 09 '23 at 11:00
0

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.

Ville Myrskyneva
  • 1,560
  • 3
  • 20
  • 35