代码之家  ›  专栏  ›  技术社区  ›  mhuelfen

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

  •  3
  • mhuelfen  · 技术社区  · 8 年前

    我正在使用spock测试Java Spring引导代码。它通过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());
      }
    }
    

    斯波克规范

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

    帮手 使用模拟记录器切换实数 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)
      }
    }
    

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

    我正在使用来自的解决方案 这个答案 ,它对断言有效,但我没有看到日志,因为它是一个模拟调用。

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

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

    但我发现它很冗长,我不知道如何为它创建一个助手函数。我对函数式groovy不是很熟悉,将此代码移到函数中是行不通的。

    我还尝试了间谍而不是模拟,但这给我带来了一个错误,因为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.
    

    运行时的Logger类

    package ch.qos.logback.classic;
    
    public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {
    

    谢谢

    2 回复  |  直到 8 年前
        1
  •  4
  •   kriegaex    6 年前

    实际上在你的 MCVE 您希望 warn(_, _) 方法,但您在 Clazz ,所以要么你必须改变 克拉兹 也可以记录异常或更改测试以期望使用一个参数调用方法。我在这里做后者。

    至于你的问题,解决方法是不要使用模拟,而是使用间谍。不过,你需要告诉斯波克你想监视哪个类。当然,这是因为您无法监视接口类型。我选择了 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
      }
    }
    

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


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

        2
  •  2
  •   Mark Bramnik    6 年前

    对于这类问题,我想与大家分享一种更具“创造性”的方法。

    您可以创建一个“人造”附加器,而不是模拟记录器,而是通过编程将其添加到测试类中的记录器中。

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

    在我看来,这种方法比广泛的模仿更容易使用,但这当然是一个品味问题。