【问题标题】:Unit testing of a class with StaticLoggerBinder使用 StaticLoggerBinder 对类进行单元测试
【发布时间】:2014-07-29 18:27:34
【问题描述】:

我确实有一个像这样的简单课程:

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):

【问题讨论】:

  • 你试过只用 1*logger.error(_) 吗?如果将输出添加到这些测试中也会很有帮助。
  • 我添加了调用。不幸的是, 1*logger.error(_) 也不能正常工作。
  • 某些用例(可能是上面的示例)的替代方法是使用docs.groovy-lang.org/next/html/gapi/groovy/lang/…

标签: unit-testing groovy mocking slf4j spock


【解决方案1】:

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

然而,这并不容易实现,因为生成的代码对测试并不友好。

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

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 中描述的“可怕的技巧”在您的测试中设置新的字段值。 IE。使用反射使字段可访问并设置值。请记住在清理过程中将值重置为原始值。
  2. getLog() 方法添加到ErrorLogger 类并使用该方法进行访问,而不是直接访问字段。然后你可以操纵metaClass 来覆盖getLog() 实现。这种方法的问题在于,您必须修改生产代码并添加一个 getter,这有悖于首先使用 @Slf4j 的目的。

我还想指出您的ErrorLoggerSpec 课程存在几个问题。这些被你已经遇到的问题所掩盖,所以当它们表现出来时,你可能会自己弄清楚。

虽然是 hack,但我只提供第一个建议的代码示例,因为第二个建议修改了生产代码。

为了隔离 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
    }
}

【讨论】:

  • @Opal 谢谢!深入研究这一点既有趣又有趣。 :)
  • 非常感谢您的出色回答!我知道我的代码中存在一些错误,因为所有这些都是虚拟的假代码,显示了我遇到的问题:)
  • @MateuszChrzaszcz 是的,我认为创建代码是为了解决问题。因为无论如何我都必须修复错误才能测试解决方案,所以我选择包含规范的固定版本。 :)
  • @Steinar 这是一个很好的答案,可以解决很多人的问题。如果你不介意的话,我会建议这个 hack 成为 Grails 3.3.x 测试框架的一部分,并且会进行 PR,因为它可以防止大量的测试代码被破坏。
  • 太棒了!这仍然适用于 Grails 4 :)
【解决方案2】:

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

@Rule
OutputCaptureRule outputCaptureRule = new OutputCaptureRule()

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

【讨论】:

  • 这正是我所需要的。谢谢!
猜你喜欢
  • 2017-05-17
  • 2012-07-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-08
  • 2020-01-17
相关资源
最近更新 更多