Group Testing a Class Using StaticLoggerBinder

I have a simple class:

package com.example.howtomocktest import groovy.util.logging.Slf4j import java.nio.channels.NotYetBoundException @Slf4j class ErrorLogger { static void handleExceptions(Closure closure) { try { closure() }catch (UnsupportedOperationException|NotYetBoundException ex) { log.error ex.message } catch (Exception ex) { log.error 'Processing exception {}', ex } } } 

And I would like to write a test for him, here is the skeleton:

 package com.example.howtomocktest import org.slf4j.Logger import spock.lang.Specification import java.nio.channels.NotYetBoundException import static com.example.howtomocktest.ErrorLogger.handleExceptions class ErrorLoggerSpec extends Specification { private static final UNSUPPORTED_EXCEPTION = { throw UnsupportedOperationException } private static final NOT_YET_BOUND = { throw NotYetBoundException } private static final STANDARD_EXCEPTION = { throw Exception } private Logger logger = Mock(Logger.class) def setup() { } def "Message logged when UnsupportedOperationException is thrown"() { when: handleExceptions {UNSUPPORTED_EXCEPTION} then: notThrown(UnsupportedOperationException) 1 * logger.error(_ as String) // doesn't work } def "Message logged when NotYetBoundException is thrown"() { when: handleExceptions {NOT_YET_BOUND} then: notThrown(NotYetBoundException) 1 * logger.error(_ as String) // doesn't work } def "Message about processing exception is logged when standard Exception is thrown"() { when: handleExceptions {STANDARD_EXCEPTION} then: notThrown(STANDARD_EXCEPTION) 1 * logger.error(_ as String) // doesn't work } } 

The logger in the ErrorLogger class is provided by StaticLoggerBinder, so my question is: how do I get it to work so that these "1 * logger.error (_ as String)" checks work? I cannot find the right way to mock this logger inside the ErrorLogger class. I thought about reflection and somehow turned to it, in addition, there was an idea with mockito intuition (but how to do it if the reference to the object is not even present in this class because of this Slf4j annotation!) Thanks in advance for all your reviews and tips.

EDIT: Here is the test result, even 1 * logger.error (_) does not work.

 Too few invocations for: 1*logger.error() (0 invocations) Unmatched invocations (ordered by similarity): 
+7
unit-testing mocking groovy slf4j spock
source share
1 answer

What you need to do is replace the log field generated by @Slf4j AST transformation with your layout.

However, this is not so easy to achieve, since the generated code is not very convenient for testing.

A quick look at the generated code shows that it matches something like this:

 class ErrorLogger { private final static transient org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ErrorLogger) } 

Since the log field is declared as private final , it is not easy to replace the value with your layout. Actually it comes down to the same problem as described here . In addition, the use of this field is verified in the isEnabled() methods, therefore, for example, every time you call log.error(msg) , it is replaced by:

 if (log.isErrorEnabled()) { log.error(msg) } 

So how to solve this? I would advise you to register the problem in the groovy issue questionnaire , where you ask for a more flexible implementation of the AST conversion. However, this will not help you now.

There are several work solutions you can consider.

  • Set a new value for the field in your test using the "terrible hack" described in the stack overflow question mentioned above . That is, make the field available using reflection and set the value. Remember to reset the value for the original during cleaning.
  • Add the getLog() method to your ErrorLogger class and use this method to access instead of directly accessing the field. You can then control metaClass to override the implementation of getLog() . The problem with this approach is that you have to modify the production code and add a getter, which, in the first place, jeopardizes the use of @Slf4j .

I would also like to point out that there are several problems with your ErrorLoggerSpec class. They are hidden due to the problems you are facing, so you will probably think about it yourself when they show themselves.

Although this is a hack, I will only give a sample code for the first sentence, since the second sentence changes the production code.

To isolate the hack, enable simple reuse and don't forget about the reset value, I wrote it as a rule JUnit (which can also be used in Spock).

 import org.junit.rules.ExternalResource import org.slf4j.Logger import java.lang.reflect.Field import java.lang.reflect.Modifier public class ReplaceSlf4jLogger extends ExternalResource { Field logField Logger logger Logger originalLogger ReplaceSlf4jLogger(Class logClass, Logger logger) { logField = logClass.getDeclaredField("log"); this.logger = logger } @Override protected void before() throws Throwable { logField.accessible = true Field modifiersField = Field.getDeclaredField("modifiers") modifiersField.accessible = true modifiersField.setInt(logField, logField.getModifiers() & ~Modifier.FINAL) originalLogger = (Logger) logField.get(null) logField.set(null, logger) } @Override protected void after() { logField.set(null, originalLogger) } } 

And here is the specification, after fixing all the minor errors and adding this rule. Changes are commented in code:

 import org.junit.Rule import org.slf4j.Logger import spock.lang.Specification import java.nio.channels.NotYetBoundException import static ErrorLogger.handleExceptions class ErrorLoggerSpec extends Specification { // NOTE: These three closures are changed to actually throw new instances of the exceptions private static final UNSUPPORTED_EXCEPTION = { throw new UnsupportedOperationException() } private static final NOT_YET_BOUND = { throw new NotYetBoundException() } private static final STANDARD_EXCEPTION = { throw new Exception() } private Logger logger = Mock(Logger.class) @Rule ReplaceSlf4jLogger replaceSlf4jLogger = new ReplaceSlf4jLogger(ErrorLogger, logger) def "Message logged when UnsupportedOperationException is thrown"() { when: handleExceptions UNSUPPORTED_EXCEPTION // Changed: used to be a closure within a closure! then: notThrown(UnsupportedOperationException) 1 * logger.isErrorEnabled() >> true // this call is added by the AST transformation 1 * logger.error(null) // no message is specified, results in a null message: _ as String does not match null } def "Message logged when NotYetBoundException is thrown"() { when: handleExceptions NOT_YET_BOUND // Changed: used to be a closure within a closure! then: notThrown(NotYetBoundException) 1 * logger.isErrorEnabled() >> true // this call is added by the AST transformation 1 * logger.error(null) // no message is specified, results in a null message: _ as String does not match null } def "Message about processing exception is logged when standard Exception is thrown"() { when: handleExceptions STANDARD_EXCEPTION // Changed: used to be a closure within a closure! then: notThrown(Exception) // Changed: you added the closure field instead of the class here //1 * logger.isErrorEnabled() >> true // this call is NOT added by the AST transformation -- perhaps a bug? 1 * logger.error(_ as String, _ as Exception) // in this case, both a message and the exception is specified } } 
+15
source share

All Articles