Spock单元测试声明日志调用并查看输出 [英] Spock unit testing assert log calls and see output

查看:246
本文介绍了Spock单元测试声明日志调用并查看输出的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用spock测试Java Spring Boot代码.它通过lombok @ Slf4j批注获得了一个登录记录器.

I am using spock to test Java Spring Boot code. It gets a logback logger over the lombok @Slf4j annotation.

假人类,有通话记录

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 to switch the real with the mock logger taken from 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)
  }
}

我想测试日志呼叫,但仍会看到日志消息.

我正在使用此答案中的解决方案,它适用于断言,但我看不到日志,因为这是一个模拟通话.

I am using the solution from this answer, it works for the assertion but I don't see the log because it is a mock call.

我想出了这个解决方案,它与groovy规范的记录器进行了通话.

I came up with this solution, which does a the call with the logger of the groovy spec.

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

但是我发现它很冗长,不知道如何为它创建一个辅助函数.我对函数槽不是很熟悉,并且无法将此代码移到函数中.

But I find it verbose, any idea how I could create a helper function for it. I am not very familiar with functional groovy and moving this code into a function is not working.

我也尝试了间谍而不是模拟,但是由于记录器类是最终的,所以我得到了一个错误.

I also tried a Spy instead of a Mock but that gets me an error because the logger class is final.

  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 中,您期望使用warn(_, _)方法可以使用两个参数来调用,但是您却没有像Clazz中那样记录日志,因此必须更改Clazz来同时记录异常或更改测试以期望使用一个参数进行方法调用.我在这里做后者.

Actually in your MCVE you expect the warn(_, _) method to be called with two parameters, but you are not logging like that in Clazz, so either you have to change Clazz to also log an exception or change the test to expect a method call with one parameter. I am doing the latter here.

对于您的问题,解决方案是不使用模拟而是间谍.但是,您需要告诉Spock您想监视哪个确切的类.这是因为您当然不能监视接口类型.我选择了SimpleLogger(更改为您在应用程序中使用的内容).

As for your problem, the solution is to not use a mock but a spy. You need to tell Spock which exact class you want to spy on, though. This is because you cannot spy on an interface type, of course. I have chosen a SimpleLogger (change to whatever you use in your application).

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一起使用,而不是与classpath上的Log4J-Simple一起使用.让我们直接监视Groovy


Update: For what it is worth, here is a version which also works with LogBack-Classic instead of Log4J-Simple on the classpath. Instead of directly spying on the final class, let's just spy on a Groovy @Delegate:

还请注意,我在测试中更改为*_,以适应具有任意数量参数的warn调用.

Please also note that I changed to *_ in the test so as to accommodate to warn calls with an arbitrary number of arguments.

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实例.在那种情况下,嘲笑或监视最终类类型而不实现接口或显式使用其类名的技巧将不起作用,因为委派类不会(也不能)是最终类类型的子类,因此可以不会被注入而不是委托.


Update 2020-01-23: I just found this one again and noticed that I forgot to explain why the @Delegate solution works: because a Groovy delegate automatically implements all interfaces which the class of the delegate instance also implements by default. In this case the logger field is declared as Logger which is an interface type. This is also why e.g. Log4J or Logback instances can be used based on the configuration. The trick of mocking or spying on a final class type not implementing an interface or used explicitly with its class name would not work in that case because the delegating class would not (and could not) be a subclass of the final class type and thus could not be injected instead of the delegate.

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

Update 2020-04-14: I did not mention before that if you don't want to spy on a real logger but simply use a dummy you can check interactions on, just use a regular Spock mock on the org.slf4j.Logger interface: def logger = Mock(Logger) That is actually the simplest solution and you don't clutter your test log with exception stack traces and other log output. I was so focused on helping the OP with his spy solution that I did not mention this before.

这篇关于Spock单元测试声明日志调用并查看输出的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆