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

问题描述 投票:1回答:1

我正在使用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());
  }
}

Spock Spec

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

Helper用this answer取得的模拟记录器切换真实。

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="https://stackoverflow.com/a/25031713/3573038">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)
  }
}

我想测试日志调用,但仍然看到日志消息。

我正在使用this answer的解决方案,它适用于断言,但我没有看到日志,因为它是一个模拟调用。

我提出了这个解决方案,它使用groovy规范的记录器进行调用。

 1 * logger.warn(_ , _) >> {
   msg, ex -> log.warn(msg, ex)
 }

但我发现它很冗长,任何想法如何为它创建一个辅助函数。我不熟悉函数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.

运行时的Logger类

package ch.qos.logback.classic;

public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {

谢谢

unit-testing logging groovy functional-programming spock
1个回答
1
投票

实际上在你的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(_)
  }
}

更新:对于它的价值,这里有一个版本,它也适用于类路径上的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
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.