Groovy&Spock - 使用@ Slf4j AST标记,检测日志记录活动

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

我正在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(),start(javafx.stage.Stage),start(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
2个回答
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级别 - 这意味着您的测试无法识别是否有任何实际记录到appender的内容。其次,在某些时候你可以在你测试的方法中添加另一个log.error() - 它不会改变你的类或方法提供的任何东西,但单元测试开始失败,因为你假设有一个log.error()的单一调用。

如果您不相信这些论点,您可以在测试中应用黑客攻击。你不能模拟ch.log字段,但如果你看一下它实例化的类(org.slf4j.impl.SimpleLogger)和log.error()最后调用的内容,你可以发现它从以下内容获取PrintStream对象:

CONFIG_PARAMS.outputChoice.getTargetPrintStream()

因为CONFIG_PARAMS.outputChoice不是最终字段,你可以用模拟替换它。你仍然无法检查log.error()是否被调用,但你可以检查被模拟的PrintStream是否多次调用.println(String str)方法。这是一个非常难看的解决方案,因为它依赖于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")
        }
    }
}

但是我希望你不会这样。

Update: making logging/reporting an important part of the feature we are implementing

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

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

class ConsoleHandler {
    private final Logger logger

    ConsoleHandler(Logger logger) {
        this.logger = logger
    }
}

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


0
投票

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

如果你认为直接使用这个@Slf4j AST Logger来监控记录是可以接受的,那么如果不使用一些人为的,脆弱的(用他的话说)丑陋的话,这是不可能的。任何解决方法似乎意味着您必须使用其他一些可模拟对象并从中委托。

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

在app类中:

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( _, _ )
© www.soinside.com 2019 - 2024. All rights reserved.