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

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

问题描述

我有这样一个简单的类:

  package com.example.howtomocktest 

import groovy.util.logging.Slf4j
import java.nio.channels.NotYetBoundException
$ b @ Slf4j
class ErrorLogger {
static void handleExceptions(Closure closure){
try {
closure()
} catch(UnsupportedOperationException | NotYetBoundException ex){
log.error ex.message
} catch(Exception ex){
log.error'处理异常{}',前
}
}
}

我想为它写一个测试,下面是一个框架:

  package com.example。 howtomocktest 

import org.slf4j.Logger
import spock.lang.Specification
import java.nio.channels.NotYetBoundException
import static com.example.howtomocktest.ErrorLogger .handleExceptions
$ b $ class ErrorLoggerSpec扩展了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引发UnsupportedOperationException异常时记录的消息(){
when:
handleExceptions {UNSUPPORTED_EXCEPTION}

then:
notThrown(UnsupportedOperationException)
1 * logger.error(_ as String)//不起作用


def在抛出NotYetBoundException时记录的消息(){
when:
handleExceptions {NOT_YET_BOUND}

then:
notThrown (NotYetBoundException)
1 * logger.error(_ as String)//不起作用
}

def在引发标准异常时记录有关处理异常的消息 ( ){
when:
handleExceptions {STANDARD_EXCEPTION}

then:
notThrown(STANDARD_EXCEPTION)
1 * logger.error(_ as String)//不起作用


$ / code>

提供ErrorLogger类中的记录器通过StaticLoggerBinder,所以我的问题是 - 我如何使其工作,以便这些检查1 * logger.error(_作为字符串)将工作?我找不到在ErrorLogger类中嘲笑该记录器的正确方法。我曾考虑过反射,并以某种方式访问​​它,此外还有一个关于mockito注入的想法(但是,如果由于Slf4j注解而引用某个对象甚至不在该类中,那么该如何实现这一点)!在此先感谢您的所有反馈和建议。



编辑:这是一个测试的输出,甚至1 * logger.error(_)不起作用。

 太少的调用:

1 * logger.error()(0调用)

不匹配的调用(按相似性排序):


解决方案

do是用你的模拟替换 @ Slf4j AST转换生成的 log 字段。



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



快速查看生成的代码显示它对应于类似这样的内容:

  class ErrorLogger { 
私有最终静态瞬态org.slf4j.Logger日志=
org.slf4j.LoggerFactory.getLogger(ErrorLogger)
}
log
字段被声明为 private final ,所以

用你的模拟来取代价值并不那么容易。它实际上归结为此处所述的完全相同的问题。另外,这个字段的用法被封装在 isEnabled()方法中,所以例如每次调用 log.error(msg)它被替换为:

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

那么,如何解决这个问题呢?我建议您在常规问题跟踪器上注册一个问题,您需要进行更多测试AST转换的友好实现。然而,这对你现在无能为力。



有几种解决方法可供您考虑。


  1. 使用在上面提到的堆栈溢出问题中。即使用反射使该字段可访问并设置值。清理过程中记得将值重置为原始值。

  2. getLog()方法添加到 ErrorLogger 类并使用该方法进行访问而不是直接访问字段。然后你可以操作 metaClass 来覆盖 getLog()实现。这种方法的问题是你必须修改生产代码并添加一个getter,这首先反对使用 @ Slf4j 的目的。

我还想指出,您的 ErrorLoggerSpec 类。这些隐藏在你已经遇到的问题中,所以你可能会在自己表现出自己的时候自己去解决这些问题。



尽管它是一种破解,我只会提供第一个建议的代码示例,因为第二个建议会修改生产代码。



要隔离hack,启用简单重用并避免忘记重置值,我写它作为一个JUnit规则(也可以在Spock中使用)。

  import org.junit.rules.ExternalResource 
import org.slf4j.Logger
import java.lang.reflect.Field $ b $ import java.lang.reflect.Modifier
$ b $ public class ReplaceSlf4jLogger extends ExternalResource {
字段logField
记录器记录器
记录器originalLogger

ReplaceSlf4jLogger(类logClass,记录器记录器){
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)
}
$ b $ @覆盖
保护void after() {
logField.set(null,originalLogger)
}

}

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

  import org.junit.Rule 
import org.slf4j.Logger
import spock.lang.Specification
import java.nio.channels.NotYetBoundException
导入静态ErrorLogger.handleExceptions

类ErrorLoggerSpec扩展了规范{

/ /注意:这三个闭包被更改为实际抛出异常的新实例
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)
$ b $ Rule @Schema ReplaceSlf4jLogger replaceSlf4jLogger = new ReplaceSlf4jLogger(ErrorLogger,记录器)

def引发UnsupportedOperationException时记录的消息(){
when:
handleExceptions UNSUPPORTED_EXCEPTION // Chan ged:曾经是闭包内的闭包!
then:
notThrown(UnsupportedOperationException)
1 * logger.isErrorEnabled()>> true //通过AST转换添加此调用
1 * logger.error(null)//不指定消息,结果为空消息:_ as String不匹配null
}

def在抛出NotYetBoundException时记录的消息(){
when:
handleExceptions NOT_YET_BOUND //已更改:曾经是闭包内的闭包!
then:
notThrown(NotYetBoundException)
1 * logger.isErrorEnabled()>> true //通过AST转换添加此调用
1 * logger.error(null)//不指定消息,结果为空消息:_ as String不匹配null
}

def在引发标准异常时记录有关处理异常的消息(){
when:
handleExceptions STANDARD_EXCEPTION //更改:用于闭包内的闭包!
then:
notThrown(Exception)//更改:您在此添加了闭包字段而不是类
// 1 * logger.isErrorEnabled()>>真正的//这个调用不被AST转换添加 - 也许是一个错误?
1 * logger.error(_ as String,_ as Exception)//在这种情况下,消息和异常都是指定的
}
}


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

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.

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

解决方案

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

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

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

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.

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

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天全站免登陆