使用 StaticLoggerBinder 对类进行单元测试 [英] Unit testing of a class with StaticLoggerBinder

查看:29
本文介绍了使用 StaticLoggerBinder 对类进行单元测试的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我确实有一个这样的简单类:

I do have a simple class like this:

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 it, here is a 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
   }
}

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

The logger in ErrorLogger class is provided by StaticLoggerBinder, so my question is - how do I make it work so that those checks "1 * logger.error(_ as String)" would work? I can't find a proper way of mocking that logger inside of ErrorLogger class. I have thought about reflection and somehow accessing it, furthermore there was an idea with mockito injection (but how to do that if reference to an object is not even present in that class because of that Slf4j annotation!) Thanks in advance for all your feedback and advices.

这是一个测试的输出,即使 1*logger.error(_) 也不起作用.

Here is an output of a test, even 1*logger.error(_) doesn't work.

Too few invocations for:

1*logger.error()   (0 invocations)

Unmatched invocations (ordered by similarity):

推荐答案

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

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

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

However, this is not so easy to achieve, since the generated code is not really test-friendly.

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

A quick look at the generated code reveals that it corresponds to something like this:

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

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

Since the log field is declared as private final it is not so easy to replace the value with your mock. It actually boils down to the exact same problem as described here. In addition, usages of this field is wrapped in isEnabled() methods, so for instance every time you invoke log.error(msg) it is replaced with:

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

那么,如何解决这个问题?我建议您在 groovy 问题跟踪器 上注册一个问题,在那里您要求进行更多测试- AST 转换的友好实现.但是,这目前对您没有多大帮助.

So, how to solve this? I would suggest that you register an issue at the groovy issue tracker, where you ask for a more test-friendly implementation of the AST transformation. However, this won't help you much right now.

您可以考虑几个变通的解决方案.

There are a couple of work-around solutions to this that you might consider.

  1. 使用描述的可怕的黑客"在您的测试中设置新字段值在上面提到的堆栈溢出问题中.IE.使用反射使字段可访问并设置值.请记住在清理期间将值重置为原始值.
  2. getLog() 方法添加到您的 ErrorLogger 类,并使用该方法进行访问而不是直接字段访问.然后您可以操作 metaClass 来覆盖 getLog() 实现.这种方法的问题在于您必须修改生产代码并添加一个 getter,这违背了使用 @Slf4j 的初衷.
  1. Set the new field value in your test using the "awful hack" described in the stack overflow question mentioned above. I.e. make the field accessible using reflection and set the value. Remember to reset the value to the original during cleanup.
  2. Add a getLog() method to your ErrorLogger class and use that method for access instead of direct field access. Then you may manipulate the metaClass to override the getLog() implementation. The problem with this approach is that you would have to modify the production code and add a getter, which kind of defies the purpose of using @Slf4j in the first place.

我还想指出您的 ErrorLoggerSpec 类有几个问题.这些都隐藏在你已经遇到的问题中,所以当它们表现出来时,你可能会自己解决.

I'd also like to point out that there are several problems with your ErrorLoggerSpec class. These are hidden by the problems you've already encountered, so you would probably figure these out by yourself when they manifested themselves.

即使是 hack,我也只会为第一个建议提供代码示例,因为第二个建议修改了生产代码.

Even though it is a hack, I'll only provide code example for the first suggestion, since the second suggestion modifies the production code.

为了隔离 hack,实现简单的重用并避免忘记重置值,我将其写为 JUnit 规则(也可以在 Spock 中使用).

To isolate the hack, enable simple reuse and avoid forgetting to reset the value, I wrote it up as a JUnit rule (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 spec, after fixing all the small bugs and adding this rule. Changes are commented in the 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
    }
}

这篇关于使用 StaticLoggerBinder 对类进行单元测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆