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

Groovy & Spock - using @Slf4j AST tag, detect logging activity

我正在 Groovy 开发应用程序。

我正在使用有用的 "AST" 标签 @Slf4j - 它会向您的 class、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: No signature of method: core.ConsoleHandler.setLog() is applicable for argument types: (ch.qos.logback.classic.Logger) values: [Logger[null]] Possible solutions: 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' )

... 这给出了 "too few invocations",尽管字符串 "dummy" 确实被记录为错误。我此时的怀疑只是 log 不能被嘲笑,因为它是通过 Groovy AST 魔法添加的 属性。

谁能想出解决办法?除了,也许,一个不优雅的包装器 class 将日志消息转发到 AST log?

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

groovy.lang.MissingMethodException: No signature of method: core.ConsoleHandler.setLog() applicable for argument types: (ch.qos.logback.classic.Logger) (...)

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

ch.@log = Mock(Logger)

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

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

如果您不相信这些论点,您可以对您的测试进行黑客攻击。您不能模拟 ch.log 字段,但是如果您看一下 class 它实例化的内容 (org.slf4j.impl.SimpleLogger) 以及 log.error() 最后调用的内容,您会发现它从以下位置获取 PrintStream 个对象:

CONFIG_PARAMS.outputChoice.getTargetPrintStream()

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

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

不过我希望你不要那样做。

更新:使logging/reporting成为我们正在实施的功能的重要组成部分

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

但是,如果日志记录对您的 class 很重要,并且您想测试被测 class 与其依赖项之间的交互,则跳过 @Slf4j 注释并提供记录器作为构造函数参数:

class ConsoleHandler {
    private final Logger logger

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

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

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

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

刚好找到一个有用的classorg.slf4j.helpers.SubstituteLogger。它有点打败了对象,因为可以说你也可以用正常的方式创建一个 Logger ......但这是包装 AST log 的一个可能的想法所以你可以检查它被要求做的一些事情。注意这个 AST log 提供的不仅仅是按照 Java 的日志记录:你还可以根据日志级别执行闭包(参见 Action 2nd Ed[= 中的 Groovy 33=], p.252 NB 在网上找了一个 link 没有成功:如果有,请编辑...)

在应用程序中 class:

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( _, _ )