Spock 单元测试断言日志调用并查看输出

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)

谢谢

kri*_*aex 5

实际上,在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.Loggerdef logger = Mock(Logger)这实际上是最简单的解决方案,并且您不会因异常堆栈跟踪和其他日志输出而使测试日志变得混乱。我非常专注于帮助OP使用他的间谍解决方案,所以我之前没有提到这一点。

  • 我使用了你的代码,在任何地方都看不到最终的 Logback 记录器,只有 Log4J 记录器。您仅在与 MCVE 无关的小片段中提到了它。看来 MCVE 实际上并不是真正的一个。因此,除非您让我可以重现该问题,否则我该如何帮助您?我不喜欢浪费时间。我的解决方案适用于您的代码!因此,如果您的代码不同,那么您的问题就是问题,而不是我的答案。PS:我回答后**您编辑了代码。 (2认同)