使用StaticLoggerBinder对类进行单元测试

问题描述 投票:9回答:2

我确实有一个像这样的简单班级:

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类内部模拟该记录器的正确方法。我已经考虑过反射并以某种方式进行访问,此外,还有一个使用模仿注入的想法(但是,如果由于该Slf4j注释而在该类中甚至没有引用对象,该怎么办!)预先感谢您的所有反馈和建议。

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

Too few invocations for:

1*logger.error()   (0 invocations)

Unmatched invocations (ordered by similarity):
unit-testing groovy mocking slf4j spock
2个回答
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转换进行更易于测试的实现。但是,这现在对您无济于事。

您可能会考虑采用几种解决方法,]。>

    使用in the stack overflow question mentioned above中所述的“可怕的骇客”在测试中设置新字段值。即使用反射使字段可访问并设置值。请记住在清除过程中将值重置为原始值。
  1. getLog()类添加ErrorLogger方法,并使用该方法进行访问,而不是直接进行字段访问。然后,您可以操纵metaClass以覆盖getLog()实现。这种方法的问题是,您将不得不修改生产代码并添加吸气剂,这首先违背了使用@Slf4j的目的。
  2. 我还要指出,您的ErrorLoggerSpec类存在一些问题。这些已经被您已经遇到的问题所隐藏,因此,当它们表现出来时,您可能会自己解决这些问题。

尽管是hack,但我只会为第一个建议提供代码示例,因为第二个建议会修改生产代码。

为了隔离黑客,实现简单的重用,并避免忘记重置值,我将其写为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 } }

如果使用的是Spring,则具有OutputCaptureRule的权限

@Rule OutputCaptureRule outputCaptureRule = new OutputCaptureRule() def test(){ outputCaptureRule.getAll().contains("<your test output>") }


0
投票
如果使用的是Spring,则具有OutputCaptureRule的权限
© www.soinside.com 2019 - 2024. All rights reserved.