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



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

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

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

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: 没有方法的签名: 核心。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魔法添加的属性。

谁能想到解决方案?除了,也许,一个不优雅的包装类,它将日志消息转发到 ASTlog

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

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

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

ch.@log = Mock(Logger)

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

通常,不应测试记录器是否在要测试的功能中记录了任何消息。基本上是因为它超出了您当前测试单元的范围,并且如果涉及到您的方法返回的内容 - 是否记录了任何内容并不重要。此外,您甚至不知道是否启用了ERROR级别 - 这意味着您的测试将无法识别是否实际记录到追加器中。其次,在某个时间点,您可以向测试的方法添加另一个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)方法调用了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实例,你就准备好了。但是,只有当测试这些交互从业务角度有意义并且这些调用对于履行与要测试的类的合同是强制性的时,它才有意义。否则就没有多大意义了。

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

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

我只是碰巧找到了一个有用的类org.slf4j.helpers.SubstituteLogger.它在某种程度上击败了对象,从某种意义上说,你可以以正常的方式创建一个Logger......但这是包装 ASTlog的一个可能的想法,这样您就可以检查它被要求做的一些事情。 注意,这个 ASTlog提供的不仅仅是根据 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( _, _ )

最新更新