使用StaticLoggerBinder对类进行单元测试

时间:2014-07-29 18:27:34

标签: unit-testing groovy mocking slf4j spock

我有一个像这样的简单类:

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
        }
    }
}

我想为它编写测试,这是一个骨架:

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
   }
}

ErrorLogger类中的记录器是由StaticLoggerBinder提供的,所以我的问题是 - 如何让它工作以便那些检查“1 * logger.error(_ as String)”能够工作?我找不到在ErrorLogger类中模拟该记录器的正确方法。我已经考虑过反射并以某种方式访问​​它,此外还有一个使用mockito注入的想法(但是如果因为Slf4j注释而在该类中甚至不存在对象的引用,那该怎么办呢!)提前感谢您的所有反馈和建议。

编辑:这是测试的输出,即使是1 * logger.error(_)也不起作用。

Too few invocations for:

1*logger.error()   (0 invocations)

Unmatched invocations (ordered by similarity):

2 个答案:

答案 0 :(得分:21)

您需要做的是用模拟替换log AST转换生成的@Slf4j字段。

然而,这并不容易实现,因为生成的代码并不是真正适合测试的。

快速查看生成的代码会发现它对应于以下内容:

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

由于log字段被声明为private final,因此用模拟替换值并不容易。它实际上归结为与here描述的完全相同的问题。此外,此字段的用法包含在isEnabled()方法中,因此,例如,每次调用log.error(msg)时,它都将替换为:

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

那么,如何解决这个问题呢?我建议您在groovy issue tracker注册一个问题,在那里您要求更加测试友好的AST转换实现。但是,这对你现在没什么帮助。

您可以考虑使用以下几种解决方案。

  1. 使用描述为in the stack overflow question mentioned above的“糟糕黑客”在测试中设置新字段值。即使用反射可以访问该字段并设置该值。请记住在清理期间将值重置为原始值。
  2. getLog()类添加ErrorLogger方法,并使用该方法进行访问,而不是直接访问字段。然后,您可以操纵metaClass来覆盖getLog()实施。这种方法的问题在于您必须修改生产代码并添加一个getter,这首先违背了使用@Slf4j的目的。
  3. 我还想指出你的ErrorLoggerSpec课有几个问题。这些都是你已经遇到过的问题所隐藏的,所以当你们表现出来时,你可能会自己想出这些问题。

    即使它是一个黑客,我只会提供第一个建议的代码示例,因为第二个建议修改了生产代码。

    要隔离黑客,启用简单重用并避免忘记重置值,我将其写成JUnit规则(也可以在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)
        }
    
    }
    

    这是规范,在修复所有小错误并添加此规则之后。更改在代码中注释:

    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
        }
    }
    

答案 1 :(得分:0)

如果您使用的是Spring,则可以使用OutputCaptureRule

@Rule
OutputCaptureRule outputCaptureRule = new OutputCaptureRule()

def test(){
outputCaptureRule.getAll().contains("<your test output>")
}