【问题标题】:Groovy & Spock - using @Slf4j AST tag, detect logging activityGroovy 和 Spock - 使用 @Slf4j AST 标签,检测日志记录活动
【发布时间】:2018-02-25 20:12:27
【问题描述】:

我正在用 Groovy 开发一个应用程序。

我正在使用有用的“AST”标签@Slf4j - 它为您的类添加了一个新属性log。我已将其配置为ch.qos.logback.classic.Logger。

现在我想测试(使用 Spock)是否记录了 error 级别的消息。注意start 方法调用loop 方法。这是受到this question的启发:

import org.slf4j.Logger
...

ConsoleHandler ch = Spy( ConsoleHandler )
ch.setMaxLoopCount 3
ch.loop() >> { throw new Throwable() }
ch.log = Mock( Logger )

when:
ch.start()

then:
1 * ch.log.error( 'dummy' )

失败了……

groovy.lang.MissingMethodException:没有方法签名: core.ConsoleHandler.setLog() 适用于参数类型: (ch.qos.logback.classic.Logger) 值:[Logger[null]] 可能 解决方案:stop()、setMode(java.lang.Object)、getMode()、 开始(javafx.stage.Stage),开始(javafx.stage.Stage), getAt(java.lang.String)

在你问之前,我也试过了:

ConsoleHandler ch = Spy( ConsoleHandler )
ch.setMaxLoopCount 3
ch.loop() >> { throw new Throwable() }
Logger mockLogger = Mock( Logger )
ch.getLog() >> mockLogger

when:
ch.start()

then:
1 * mockLogger.error( 'dummy' )

...这给出了“太少的调用”,尽管字符串“dummy”确实被记录为错误。在这一点上我的怀疑只是 log 不能被嘲笑,因为它是通过 Groovy AST 魔法添加的属性。

谁能想到解决办法?除了将日志消息转发到 AST log 的不优雅的包装类之外,可能还有其他?

【问题讨论】:

    标签: logging gradle groovy abstract-syntax-tree


    【解决方案1】:

    您不能覆盖 log 字段,因为它是最终静态字段。你看这个

    groovy.lang.MissingMethodException:没有方法签名:core.ConsoleHandler.setLog() 适用于参数类型:(ch.qos.logback.classic.Logger) (...)

    例外,因为 final 字段没有任何 setter 方法。如果它不是最终字段,您可以尝试使用以下方法覆盖它:

    ch.@log = Mock(Logger)
    

    @在这种情况下意味着你​​想直接访问对象字段(Groovy 在访问值时将ch.log 编译为ch.getLog(),在修改字段时将ch.setLog())。

    一般来说,您不应该测试 logger 是否在您正在测试的功能中记录了任何消息。基本上是因为它超出了您当前正在测试的单元的范围,并且如果涉及到您的方法返回的内容 - 是否记录任何内容都没有关系。此外,您甚至不知道是否启用了 ERROR 级别 - 这意味着您的测试将无法识别是否有任何内容实际记录到附加程序中。其次,在某个时间点,您可以将另一个 log.error() 添加到您测试的方法中 - 它不会改变您的类或方法提供的任何内容,但是单元测试开始失败,因为您假设有一个单独的调用log.error().

    如果您不相信这些论点,您可以在测试中应用 hack。你不能模拟 ch.log 字段,但是如果你看看它实例化了什么类 (org.slf4j.impl.SimpleLogger) 以及 log.error() 最后调用了什么,你会发现它从以下位置获取 PrintStream 对象:

    CONFIG_PARAMS.outputChoice.getTargetPrintStream()
    

    而且因为CONFIG_PARAMS.outputChoice 不是final 字段,您可以将其替换为mock。您仍然无法检查是否调用了 log.error(),但是您可以检查模拟的 PrintStream 是否调用了 .println(String str) 方法 n 次。这是一个非常丑陋的解决方案,因为它依赖于 org.slf4j.impl.SimpleLogger 类的内部实现细节。我将这种解决方法称为问自己一个问题,因为您将测试与当前的org.slf4j.impl.SimpleLogger 实现紧密结合——很容易想象几个月后您将 Slf4j 更新为更改log.error() 实现的版本和您的测试在没有战略原因的情况下开始失败。这是这个肮脏的解决方法的样子:

    import groovy.util.logging.Slf4j
    import org.slf4j.impl.OutputChoice
    import spock.lang.Specification
    
    class SpyMethodArgsExampleSpec extends Specification {
    
        def "testing logging activity, but why?"() {
            given:
            ConsoleHandler ch = Spy(ConsoleHandler)
    
            PrintStream printStream = Mock(PrintStream)
            ch.log.CONFIG_PARAMS.outputChoice = Mock(OutputChoice)
            ch.log.CONFIG_PARAMS.outputChoice.getTargetPrintStream() >> printStream
    
            when:
            ch.run()
    
            then:
            1 * printStream.println(_ as String)
        }
    
        @Slf4j
        static class ConsoleHandler {
            void run() {
                log.error("test")
            }
        }
    }
    

    但我希望你不要那样做。

    更新:使日志记录/报告成为我们正在实施的功能的重要组成部分

    假设日志记录/报告部分对您的课程至关重要,那么在这种情况下,值得重新考虑您的课程设计。将类依赖项定义为构造函数参数是一种很好的做法 - 您在初始化级别显式表达类依赖项。使用@Slf4j 是添加静态最终记录器的非常方便的方法,但在这种情况下,它是实现级别的详细信息,从公共客户端的角度来看是不可见的。这就是为什么测试这些内部细节非常棘手的原因。

    但是,如果日志记录对您的类很重要,并且您想测试被测类及其依赖项之间的交互,那么跳过 @Slf4j 注释并提供 logger 作为构造函数参数并没有错:

    class ConsoleHandler {
        private final Logger logger
    
        ConsoleHandler(Logger logger) {
            this.logger = logger
        }
    }
    

    当然它也有缺点——你需要在任何时候创建ConsoleHandler 类的实例时传递它。但这使它完全可测试 - 在您的测试中,您只需模拟 Logger 实例,您就可以开始了。但只有从业务角度测试这些交互是有意义的,并且这些调用对于履行与您正在测试的类的合同是强制性的,才有意义。否则没有多大意义。

    【讨论】:

    • 谢谢...我不会!因此,如果我理解正确,所有日志记录活动基本上都被视为“测试范围之外”。我正在努力解决这个问题,因为正确的报告/记录(尤其是出错的事情)肯定是应用程序的重要组成部分。关于其他“log.error”消息干扰的可能性:我计划通过(希望)捕获记录器错误级别的输出并针对正则表达式对其进行测试来解决这个问题。但是现在我不确定该怎么想:通过(可测试的)“logError”方法进行间接寻址似乎也很垃圾......! (叹气!)
    • @mikerodent 这取决于。如果在错误/信息/调试级别记录只是一个实现细节并且不会影响您的类提供的业务逻辑 - 测试这些交互是没有意义的。但另一方面 - 如果日志记录是要求的一部分,您可以通过期待 Logger 作为构造函数参数来“收缩”ConsoleHandler 与 Logger - 所有客户端都知道在这种情况下,使用 ConsoleHandler 需要有效记录器。在这些情况下,您的测试可能会涵盖强制性交互。您只需公开此合同,而不是将其保留为实现细节
    • 啊,对……我明白了。再次感谢,这是一个非常有价值的解释。我经常感到困惑的另一件事是,我使用 TDD 进行开发的方式,在测试失败之前我不会真正编写任何应用程序代码......通常是一个新的测试。所以它可能不仅是我的测试涵盖的“业务逻辑”......他们希望覆盖......一切。如果我不以这种方式进行,那真的是 TDD 吗?我真的不期待答案......我只是想把它弄明白...... :)
    • @mikerodent 绝对。 TDD 对于你应该测试什么单元并不是教条主义的——它是一个类、一个方法还是其他东西。就个人而言 - 我喜欢测试用例,反映应用程序中业务流程的场景。有时这意味着我在单个类的范围内编写测试,有时我测试整个包,因为它封装了我想用 TDD 设计的整个业务流程。一般来说 - 做最适合您和项目的事情。
    【解决方案2】:

    Szymon Stepniak 提出了一个非常巧妙的解决方案,但建议我不要使用它...出于他在上一段中解释的所有正确原因(以及与我的讨论)。

    如果您认为希望使用此@Slf4j AST Logger 直接监控日志记录是可以接受的事情,如果不使用某些东西,这似乎是不可能的做作,脆弱和(用他的话)丑陋。任何解决方法似乎都意味着您必须使用其他一些可模拟对象并从中委托。

    我刚好找到一个有用的类org.slf4j.helpers.SubstituteLogger。从某种意义上说,它有点打败了对象,从某种意义上说,你还不如以正常方式创建一个Logger……但这是包装 AST log 的一个可能的想法,所以你可以检查一下它被要求做的一些事情。注意这个 AST log 提供的不仅仅是按照 Java 进行日志记录:您还可以执行依赖于日志级别的闭包(请参阅 Groovy in Action 2nd Ed,第 252 页 NB 寻找在线链接没有成功:如果有,请编辑...)

    在应用类中:

    Console Handler {
        SubstituteLogger substLog
    
        ConsoleHandler(){
            substLog = new SubstituteLogger( 'substLog', new ArrayDeque() /* for example */, true )
            substLog.delegate = log
        }
    
        ...
    
            log.info( "something banal which you don't want to test..." )
        ... 
    
        }catch( Exception e ){
            substLog.error( 'oops', e )
        }
    

    测试方法:

        ConsoleHandler ch = Spy( ConsoleHandler )
        ch.loop() >> { throw new Throwable() }
        Logger mockLogger = Mock( SubstituteLogger )
        ch.substLog = mockLogger
    
        when:
        ch.start()
    
        then:
        1 * mockLogger.error( _, _ )
    

    【讨论】:

      猜你喜欢
      • 2018-03-28
      • 2017-05-06
      • 1970-01-01
      • 1970-01-01
      • 2013-01-21
      • 2012-08-26
      • 2011-11-07
      • 2018-10-07
      • 2021-05-26
      相关资源
      最近更新 更多