【问题标题】:Spock unit testing assert log calls and see outputSpock 单元测试断言日志调用并查看输出
【发布时间】:2018-09-21 03:39:24
【问题描述】:

我正在使用 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 规范

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,但由于 logger 类是最终类,所以会出错。

  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.

运行时的记录器类

package ch.qos.logback.classic;

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

谢谢

【问题讨论】:

  • 请分享MCVE。 “我正在做与其他人相似的事情,但它并没有达到我的预期”几乎不能作为一个可以回答的问题。我喜欢查看您的 Spock 规范以及正在测试的代码,以便能够重现和理解您的问题。如果这要求太多,请单独解决您的问题。
  • 我添加了一个 MCVE。
  • 不,你没有。
  • 抱歉,我错过了有关规范的部分。我现在添加了工作规范、虚拟类和助手。

标签: unit-testing logging groovy functional-programming spock


【解决方案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。与其直接监视 final 类,不如监视一个 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
  }
}

2020-01-23 更新:我刚刚又找到了这个,发现我忘了解释为什么 @Delegate 解决方案有效:因为 Groovy 委托会自动实现类的所有接口委托实例也默认实现。在这种情况下,记录器字段被声明为Logger,这是一个接口类型。这也是为什么例如可以根据配置使用 Log4J 或 Logback 实例。在这种情况下,模拟或监视未实现接口或显式使用其类名的最终类类型的技巧将不起作用,因为委托类不会(也不能)是最终类类型的子类,因此可以不被注入而不是委托。


2020-04-14 更新:我之前没有提到,如果您不想监视真正的记录器,而只是使用可以检查交互的假人,只需使用常规org.slf4j.Logger 接口上的 Spock 模拟:def logger = Mock(Logger) 这实际上是最简单的解决方案,您不会因为异常堆栈跟踪和其他日志输出而使测试日志混乱。我非常专注于帮助 OP 解决他的间谍解决方案,我之前没有提到这一点。

【讨论】:

  • 我尝试了间谍方法,但我在运行时使用了最终的 Logger 类,请参阅问题的结尾。因此,如果我在测试中使用另一个 Logger 类作为 Spy,则输出可能会有所不同。这就是为什么我产生了构建一个辅助函数来断言日志调用并再次记录它的想法的原因。
  • 我使用了你的代码,没有任何地方可以看到最终的 Logback 记录器,只有一个 Log4J 记录器。您只在与 MCVE 无关的小 sn-p 中提到它。看起来 MCVE 实际上并不是一个。所以除非你让我的问题可以重现,否则我应该如何帮助你?我不喜欢浪费时间。我的解决方案适用于您的代码!因此,如果您的代码不同,那么您的问题就是问题,而不是我的答案。 P.S.:您在我回答之后 编辑了您的代码。
  • 另一点:如果输出不同,对您的测试来说并不重要。重要的是使用预期参数调用记录器方法,您可以使用我的方法进行测试。此外,LogBack Classic 和 Slf5J-Simple 都实现了相同的接口,因此它们是相互替代的。而且您自己的解决方案也直接模拟接口,它也不会将 LogBack-Classic 实例注入Clazz
  • 更新:如果您想知道如何在 Groovy @Delegate 上使用 Spock Spy 而不是直接监视最终类,请参阅我的第二个示例测试。我希望你现在快乐。
  • 感谢您的努力。这对我有用,我确实很高兴!
【解决方案2】:

这是我想分享的解决此类问题的另一种“创造性”方法。

您可以创建一个“人工”附加程序,而不是模拟记录器,而是以编程方式将其添加到被测类的记录器中。

appender 将跟踪记录的消息,并且在验证阶段您将获得这些记录的消息并进行验证

你最终会得到这样的东西(伪代码只是为了展示这个想法):


class MsgTrackingAppender implements Appender { // Appender of your logging system
   private List<LogEvent> events = new ArrayList<>();

   public void doAppend(LogEvent evt) {
       events.add(evt);
   }

   public List<LogEvent> getEvents() {
       return events;
   }
}

// now in test you can do:
class LogSpec extends Specification {

   def "test me"() {
     given:
       Clazz underTest = Clazz()
       MsgTrackingAppender appender = new MsgTrackingAppender()
       LogFactory.getLogger(Clazz.class).addAppender(appender)
     when:
       underTest.method()
     then:
       appender.events.size == 1
       appender.events[0].level == Level.WARN
       appender.events[0].message == ... // verify whatever you want on the messages       
   }
}

IMO 这种方法比广泛的模拟更容易使用,但它当然是个人喜好问题。

【讨论】:

  • 我只是注意到了这个答案,因为我有活动。我喜欢创造性的方法,特别是如果它们是干净的。您基本上构建自己的测试替身(在 Spock 中称为存根)并将其作为依赖项(或作为协作者,选择一个术语)注入到记录器中。虽然我仍然更喜欢尽可能“平坦”地“模拟”依赖关系,但通过使用真正的记录器(这也需要其余的日志记录基础设施),你的依赖关系更深一层,注入你的“诊断附加程序”。确实很好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-17
  • 2016-06-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多