mhu*_*fen 5 groovy logging unit-testing functional-programming spock
我正在使用 spock 来测试 Java Spring Boot 代码。它通过 lombok @Slf4j 注释获取 logback 记录器。
带日志调用的虚拟类
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class Clazz {
public void method() {
// ... code
log.warn("message", new RuntimeException());
}
}
Run Code Online (Sandbox Code Playgroud)
斯波克规格
import groovy.util.logging.Slf4j
import org.junit.Rule
import org.slf4j.Logger
import spock.lang.Specification
@Slf4j
class LogSpec extends Specification {
Clazz clazz = new Clazz()
private Logger logger = Mock(Logger.class)
@Rule
ReplaceSlf4jLogger replaceSlf4jLogger = new ReplaceSlf4jLogger(Clazz, logger)
def "warning ia logged"() {
given: "expected message"
when: "when calling the method"
clazz.method()
then: "a warning is logged"
1 * logger.warn(_, _) >> {
msg, ex -> log.warn(msg, ex)
}
}
}
Run Code Online (Sandbox Code Playgroud)
使用从此答案中获取的模拟记录器来帮助切换真实记录。
import org.junit.rules.ExternalResource
import org.slf4j.Logger
import java.lang.reflect.Field
import java.lang.reflect.Modifier
/**
* Helper to exchange loggers set by lombok with mock logger
*
* allows to assert log action.
*
* Undos change after test to keep normal logging in other tests.
*
* code from this <a href="/sf/answers/1752219941/">answer</a> answer
*/
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)
}
}
Run Code Online (Sandbox Code Playgroud)
我想测试日志调用,但仍然看到日志消息。
我正在使用此答案中的解决方案,它适用于断言,但我没有看到日志,因为它是模拟调用。
我想出了这个解决方案,它使用常规规范的记录器进行调用。
1 * logger.warn(_ , _) >> {
msg, ex -> log.warn(msg, ex)
}
Run Code Online (Sandbox Code Playgroud)
但我发现它很冗长,不知道如何为它创建一个辅助函数。我对函数式 Groovy 不太熟悉,将这段代码移到函数中是行不通的。
我还尝试了 Spy 而不是 Mock,但这给我带来了错误,因为记录器类是最终的。
import ch.qos.logback.classic.Logger
private Logger logger = Spy(Logger.class)
>> org.spockframework.mock.CannotCreateMockException: Cannot create mock
for class ch.qos.logback.classic.Logger because Java mocks cannot mock final classes.
If the code under test is written in Groovy, use a Groovy mock.
Run Code Online (Sandbox Code Playgroud)
运行时的记录器类
package ch.qos.logback.classic;
public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {
Run Code Online (Sandbox Code Playgroud)
谢谢
实际上,在MCVE中,您期望warn(_, _)使用两个参数调用该方法,但您没有像 中那样进行记录Clazz,因此您必须更改Clazz为也记录异常,或者更改测试以期望使用一个参数调用方法。我在这里做的是后者。
至于你的问题,解决方案是不使用模拟而是使用间谍。不过,你需要告诉 Spock 你想要监视哪个类别。当然,这是因为您无法监视接口类型。我选择了SimpleLogger(更改为您在应用程序中使用的任何内容)。
package de.scrum_master.stackoverflow
import groovy.util.logging.Slf4j
import org.junit.Rule
import org.slf4j.impl.SimpleLogger
import spock.lang.Specification
@Slf4j
class LombokSlf4jLogTest extends Specification {
SimpleLogger logger = Spy(constructorArgs: ["LombokSlf4jLogTest"])
@Rule
ReplaceSlf4jLogger replaceSlf4jLogger = new ReplaceSlf4jLogger(Clazz, logger)
def "warning is logged"() {
when: "when calling the method"
new Clazz().method()
then: "a warning is logged"
1 * logger.warn(_)
}
}
Run Code Online (Sandbox Code Playgroud)
更新:就其价值而言,这里的版本也可以在类路径上使用 LogBack-Classic 而不是 Log4J-Simple。我们不直接监视最终类,而是监视 Groovy @Delegate:
另请注意,我在测试中更改为*_,以便适应warn具有任意数量参数的调用。
package de.scrum_master.stackoverflow
import groovy.util.logging.Slf4j
import org.junit.Rule
import org.slf4j.Logger
import spock.lang.Specification
@Slf4j
class LombokSlf4jLogTest extends Specification {
def logger = Spy(new LoggerDelegate(originalLogger: log))
@Rule
ReplaceSlf4jLogger replaceSlf4jLogger = new ReplaceSlf4jLogger(Clazz, logger)
def "warning is logged"() {
when: "when calling the method"
new Clazz().method()
then: "a warning is logged"
1 * logger.warn(*_)
true
}
static class LoggerDelegate {
@Delegate Logger originalLogger
}
}
Run Code Online (Sandbox Code Playgroud)
2020-01-23 更新:我刚刚再次发现这个,并注意到我忘记解释为什么该@Delegate解决方案有效:因为 Groovy 委托会自动实现委托实例的类默认情况下也实现的所有接口。在这种情况下,记录器字段被声明为Logger接口类型。这也是为什么可以根据配置使用 Log4J 或 Logback 实例。在这种情况下,模拟或监视未实现接口或显式使用其类名的最终类类型的技巧将不起作用,因为委托类不会(也不可能)是最终类类型的子类,因此可以不被注入而不是委托。
2020-04-14 更新:我之前没有提到,如果你不想监视真正的记录器,而只是使用一个虚拟记录器,你可以检查交互,只需在界面上使用常规的 Spock 模拟org.slf4j.Logger:def logger = Mock(Logger)这实际上是最简单的解决方案,并且您不会因异常堆栈跟踪和其他日志输出而使测试日志变得混乱。我非常专注于帮助OP使用他的间谍解决方案,所以我之前没有提到这一点。