【问题标题】:Mocking Logger and LoggerFactory with PowerMock and Mockito使用 PowerMock 和 Mockito 模拟 Logger 和 LoggerFactory
【发布时间】:2023-03-12 23:20:01
【问题描述】:

我有以下要模拟的 Logger,但要验证日志条目是否被调用,而不是内容。

private static Logger logger = 
        LoggerFactory.getLogger(GoodbyeController.class);

我想模拟用于 LoggerFactory.getLogger() 的任何类,但我不知道该怎么做。 到目前为止,这就是我的最终结果:

@Before
public void performBeforeEachTest() {
    PowerMockito.mockStatic(LoggerFactory.class);
    when(LoggerFactory.getLogger(GoodbyeController.class)).
        thenReturn(loggerMock);

    when(loggerMock.isDebugEnabled()).thenReturn(true);
    doNothing().when(loggerMock).error(any(String.class));

    ...
}

我想知道:

  1. 我可以模拟静态 LoggerFactory.getLogger() 来为任何班级工作吗?
  2. 我似乎只能在@Before 中运行when(loggerMock.isDebugEnabled()).thenReturn(true);,因此我似乎无法更改每个方法的特征。有没有办法解决这个问题?

编辑结果:

我以为我已经尝试过了,但没有成功:

 when(LoggerFactory.getLogger(any(Class.class))).thenReturn(loggerMock);

但是谢谢你,因为它确实有效。

但是我已经尝试了无数种变化:

when(loggerMock.isDebugEnabled()).thenReturn(true);

我无法让 loggerMock 在 @Before 之外更改其行为,但这仅发生在 Coburtura 上。使用 Clover,覆盖率显示为 100%,但无论哪种方式仍然存在问题。

我有这个简单的课程:

public ExampleService{
    private static final Logger logger =
            LoggerFactory.getLogger(ExampleService.class);

    public String getMessage() {        
    if(logger.isDebugEnabled()){
        logger.debug("isDebugEnabled");
        logger.debug("isDebugEnabled");
    }
    return "Hello world!";
    }
    ...
}

然后我有这个测试:

@RunWith(PowerMockRunner.class)
@PrepareForTest({LoggerFactory.class})
public class ExampleServiceTests {

    @Mock
    private Logger loggerMock;
    private ExampleServiceservice = new ExampleService();

    @Before
    public void performBeforeEachTest() {
        PowerMockito.mockStatic(LoggerFactory.class);
        when(LoggerFactory.getLogger(any(Class.class))).
            thenReturn(loggerMock);

        //PowerMockito.verifyStatic(); // fails
    }

    @Test
    public void testIsDebugEnabled_True() throws Exception {
        when(loggerMock.isDebugEnabled()).thenReturn(true);
        doNothing().when(loggerMock).debug(any(String.class));

        assertThat(service.getMessage(), is("Hello null: 0"));
        //verify(loggerMock, atLeast(1)).isDebugEnabled(); // fails
    }

    @Test
    public void testIsDebugEnabled_False() throws Exception {
        when(loggerMock.isDebugEnabled()).thenReturn(false);
        doNothing().when(loggerMock).debug(any(String.class));

        assertThat(service.getMessage(), is("Hello null: 0"));
        //verify(loggerMock, atLeast(1)).isDebugEnabled(); // fails
    }
}

在三叶草中,我显示了 if(logger.isDebugEnabled()){ 块的 100% 覆盖率。 但是如果我尝试验证loggerMock:

verify(loggerMock, atLeast(1)).isDebugEnabled();

我得到零互动。 我也试过PowerMockito.verifyStatic();在@Before 中,但也有零交互。

Cobertura 将if(logger.isDebugEnabled()){ 显示为不是 100% 完成,这似乎很奇怪,而 Clover 确实如此,但两者都同意验证失败。

【问题讨论】:

  • 你试过@MockPolicy吗? Examples here 用于 EasyMock 风格的模拟,但可以适用于 Mockito。

标签: java junit mockito slf4j powermock


【解决方案1】:

编辑 2020-09-21:自 3.4.0 起,Mockito 支持模拟静态方法,API 仍在孵化中,可能会发生变化,特别是在存根和验证方面。它需要mockito-inline 工件。而且您无需准备测试或使用任何特定的跑步者。您需要做的就是:

@Test
public void name() {
    try (MockedStatic<LoggerFactory> integerMock = mockStatic(LoggerFactory.class)) {
        final Logger logger = mock(Logger.class);
        integerMock.when(() -> LoggerFactory.getLogger(any(Class.class))).thenReturn(logger);
        new Controller().log();
        verify(logger).warn(any());
    }
}

此代码中的两个重要方面是您需要在应用静态模拟时确定范围,即在此 try 块内。并且您需要从MockedStatic 对象调用存根和验证API。


@Mick,也尝试准备静态字段的所有者,例如:

@PrepareForTest({GoodbyeController.class, LoggerFactory.class})

EDIT1:我只是制作了一个小例子。首先是控制器:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Controller {
    Logger logger = LoggerFactory.getLogger(Controller.class);

    public void log() { logger.warn("yup"); }
}

然后是测试:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Controller.class, LoggerFactory.class})
public class ControllerTest {

    @Test
    public void name() throws Exception {
        mockStatic(LoggerFactory.class);
        Logger logger = mock(Logger.class);
        when(LoggerFactory.getLogger(any(Class.class))).thenReturn(logger);
        
        new Controller().log();
        
        verify(logger).warn(anyString());
    }
}

注意进口! 类路径中值得注意的库:Mockito、PowerMock、JUnit、logback-core、logback-clasic、slf4j


EDIT2:由于这似乎是一个流行的问题,我想指出,如果这些日志消息非常重要并且需要进行测试,即它们是功能/业务的一部分系统然后引入一个真正的依赖项来明确这些日志是功能会在整个系统设计中变得更好,而不是依赖于标准的静态代码和记录器的技术类。

对于这个问题,我建议使用reportIncorrectUseOfYAndZForActionXreportProgressStartedForActionX 等方法制作类似Reporter 的类。这将有利于使任何阅读代码的人都可以看到该功能。但它也将有助于实现测试,更改此特定功能的实现细节。

因此,您不需要像 PowerMock 这样的静态模拟工具。 我认为静态代码可以,但是一旦测试需要验证或模拟静态行为,就需要重构并引入明确的依赖关系。

【讨论】:

  • 注意:如果你在那里尝试了第二个@Test,你会遇到问题。在附加测试中再次调用 verify() 时将不起作用。如果您使用 @ Before 或新的 var 名称,则不会。 classLoader 只会创建其中一个,因此您不能拥有两个不同的静态类。将您的测试分成单独的类。
  • 不幸的是我无法让它工作...... Mockito 说.. 实际上,与这个模拟的交互为零。
  • 对我来说很好(包括验证)。确保您使用的是 @PrepareForTest 并验证您已模拟了日志工厂的正确日志查找方法。
  • @Brice 我知道这是一个很老的问题,但我想问你一些关于“引入真正的依赖”和避免使用静态模拟工具的建议。我非常同意您关于使该功能对任何人可见的观点,但是,我认为如果您还需要对该报告类进行单元测试,您仍然需要使用这些静态模拟工具。你不会吗?
  • @beni0888 确实建议对这个类进行单元测试,但是可以使用构造函数中设置的Logger 字段来设计具体的依赖关系,因此可以通过一个纯 mockito Logger mock,在一些仅用于测试目的的构造函数中可见。
【解决方案2】:

聚会有点晚了——我正在做类似的事情,需要一些指导,最后到了这里。没有功劳——我从 Brice 那里拿走了所有的代码,但得到的“零交互”比 Cengiz 得到的要多。

使用 jheriks 和 Joseph Lust 提出的指导,我想我知道为什么 - 我将我的对象作为一个字段进行测试,并在 @Before 中更新它,这与 Brice 不同。然后实际的记录器不是模拟的,而是一个真正的类,正如 jhriks 所建议的那样......

我通常会为我的测试对象执行此操作,以便为每个测试获取一个新对象。当我将该字段移至本地并在测试中对其进行更新时,它运行正常。但是,如果我尝试第二次测试,那不是我测试中的模拟,而是第一次测试中的模拟,我再次获得了零交互。

当我在 @BeforeClass 中创建模拟时,被测对象中的记录器始终是模拟,但请参阅下面的注释以了解此问题...

被测类

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyClassWithSomeLogging  {

    private static final Logger LOG = LoggerFactory.getLogger(MyClassWithSomeLogging.class);

    public void doStuff(boolean b) {
        if(b) {
            LOG.info("true");
        } else {
            LOG.info("false");
        }

    }
}

测试

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.mockito.Mockito.*;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.*;
import static org.powermock.api.mockito.PowerMockito.when;


@RunWith(PowerMockRunner.class)
@PrepareForTest({LoggerFactory.class})
public class MyClassWithSomeLoggingTest {

    private static Logger mockLOG;

    @BeforeClass
    public static void setup() {
        mockStatic(LoggerFactory.class);
        mockLOG = mock(Logger.class);
        when(LoggerFactory.getLogger(any(Class.class))).thenReturn(mockLOG);
    }

    @Test
    public void testIt() {
        MyClassWithSomeLogging myClassWithSomeLogging = new MyClassWithSomeLogging();
        myClassWithSomeLogging.doStuff(true);

        verify(mockLOG, times(1)).info("true");
    }

    @Test
    public void testIt2() {
        MyClassWithSomeLogging myClassWithSomeLogging = new MyClassWithSomeLogging();
        myClassWithSomeLogging.doStuff(false);

        verify(mockLOG, times(1)).info("false");
    }

    @AfterClass
    public static void verifyStatic() {
        verify(mockLOG, times(1)).info("true");
        verify(mockLOG, times(1)).info("false");
        verify(mockLOG, times(2)).info(anyString());
    }
}

注意

如果您有两个具有相同期望的测试,我必须在 @AfterClass 中进行验证,因为静态上的调用堆叠在一起 - verify(mockLOG, times(2)).info("true"); - 而不是每个测试中的 times(1) 就像第二个测试那样没有说在那里 2 调用这个。这是漂亮的裤子,但我找不到清除调用的方法。我想知道是否有人能想到解决这个问题的方法....

【讨论】:

  • Markus Wendl 使用重置的建议对我有用。
  • +500。 @BeforeClass 改变了我。
【解决方案3】:

在回答您的第一个问题时,它应该像替换一样简单:

   when(LoggerFactory.getLogger(GoodbyeController.class)).thenReturn(loggerMock);

   when(LoggerFactory.getLogger(any(Class.class))).thenReturn(loggerMock);

关于您的第二个问题(可能是第一个问题的令人费解的行为),我认为问题在于记录器是静态的。所以,

private static Logger logger = LoggerFactory.getLogger(GoodbyeController.class);

class 初始化时执行,而不是在 object 实例化时执行。有时这可能几乎同时发生,所以你会没事的,但很难保证这一点。因此,您设置 LoggerFactory.getLogger 以返回您的模拟,但在设置模拟时,记录器变量可能已经设置为真实的 Logger 对象。

您可以使用ReflectionTestUtils 之类的东西显式设置记录器(我不知道这是否适用于静态字段)或将其从静态字段更改为实例字段。无论哪种方式,您都不需要模拟 LoggerFactory.getLogger,因为您将直接注入模拟 Logger 实例。

【讨论】:

    【解决方案4】:

    我认为您可以使用 Mockito.reset(mockLog) 重置调用。你应该在每次测试之前调用它,所以在 @Before 里面会是一个好地方。

    【讨论】:

      【解决方案5】:

      使用显式注入。 例如,没有其他方法可以让您在同一个 JVM 中并行运行测试。

      使用任何类加载器(如静态日志绑定器)或像 logback.XML 之类的环境思考的模式在测试时都失败了。

      考虑我提到的并行化测试,或者考虑你想要拦截组件 A 的日志记录的情况,组件 A 的构造隐藏在 api B 后面。如果你从顶部使用依赖注入的 loggerfactory,后一种情况很容易处理,但如果您注入 Logger,则不会,因为 ILoggerFactory.getLogger 的此程序集中没有接缝。

      这也不仅仅与单元测试有关。有时我们希望集成测试发出日志。有时我们不会。有人我们希望选择性地抑制某些集成测试日志记录,例如对于预期的错误,否则会使 CI 控制台混乱和混淆。如果您从主线(或您可能使用的任何 di 框架)的顶部注入 ILoggerFactory,一切都很容易

      所以...

      要么按照建议注入报告器,要么采用注入 ILoggerFactory 的模式。通过显式 ILoggerFactory 注入而不是 Logger,您可以支持许多访问/拦截模式和并行化。

      【讨论】:

        【解决方案6】:
        class SomeService{
          private static final Logger logger = LogManager.getLogger(SomeService.class);
        
          public void callSomeMethod(){
           ...
           ...
           if (logger.isDebugEnabled()) {
                logger.debug(String.format("some message ....."));
            }
          }
        
        }
        

        JUNIT 测试范围 -

        import java.io.IOException;
         
        import org.apache.logging.log4j.core.Appender;
        import org.apache.logging.log4j.core.LogEvent;
        import org.apache.logging.log4j.Level;
        import org.apache.logging.log4j.LogManager;
        import org.apache.logging.log4j.core.Logger;
        import org.junit.After;
        import org.junit.Before;
        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.mockito.Mock;
        import org.mockito.runners.MockitoJUnitRunner;
         
        import static org.junit.Assert.*;
        import static org.mockito.Mockito.*;
         
        
        @RunWith(MockitoJUnitRunner.class)
        public class SomeServiceTest {
          @Mock
          private Appender mockAppender;
         
          private Logger logger;
         
          @Before
          public void setup() {
            when(mockAppender.getName()).thenReturn("MockAppender");
            when(mockAppender.isStarted()).thenReturn(true);
            when(mockAppender.isStopped()).thenReturn(false);
         
            logger = (Logger)LogManager.getLogger(SomeService.class);
            logger.addAppender(mockAppender);
            logger.setLevel(Level.DEBUG);
          }
         
          
         
          @Test
          public void callSomeMethodTest() {
            //write test cases, logger.isDebugEnabled() will call.
          }
         
        }
        

        【讨论】:

          【解决方案7】:

          以下是一个测试类,它在类 LogUtil 中模拟名为 log 的私有静态最终 Logger。

          除了模拟getLogger工厂调用外,还需要使用反射显式设置字段,在@BeforeClass

          public class LogUtilTest {
          
              private static Logger logger;
          
              private static MockedStatic<LoggerFactory> loggerFactoryMockedStatic;
          
              /**
               * Since {@link LogUtil#log} being a static final variable it is only initialized once at the class load time
               * So assertions are also performed against the same mock {@link LogUtilTest#logger}
               */
              @BeforeClass
              public static void beforeClass() {
                  logger = mock(Logger.class);
                  loggerFactoryMockedStatic = mockStatic(LoggerFactory.class);
                  loggerFactoryMockedStatic.when(() -> LoggerFactory.getLogger(anyString())).thenReturn(logger);
                  Whitebox.setInternalState(LogUtil.class, "log", logger);
              }
          
              @AfterClass
              public static void after() {
                  loggerFactoryMockedStatic.close();
              }
          } 
          

          【讨论】:

          • 这毫无意义。如果覆盖了静态方法的返回值,为什么还需要模拟它。
          【解决方案8】:

          我可以在没有PowerMock 的情况下使用Mockito 进行一次测试。当我使用verify 进行两个测试时,当我在课堂上运行所有测试时,第二个测试开始以no interactions with this mock 失败。两个测试分别起作用。

          经过一些实验,它仅在我制作模拟记录器static时才对我有用。

          @RunWith(MockitoJUnitRunner.class)
          public class MyTest {
              private static MockedStatic<LoggerFactory> loggerFactoryMockedStatic;
              private static final Logger logger = mock(Logger.class);
              
              @BeforeClass
              public static void beforeAll() {
                  loggerFactoryMockedStatic = mockStatic(LoggerFactory.class);
                  loggerFactoryMockedStatic.when(() -> LoggerFactory.getLogger(MyServiceUnderTest.class))
                          .thenReturn(logger);
              }
          
              @AfterClass
              public static void afterAll() {
                  loggerFactoryMockedStatic.close();
              }
          
          
              @Before
              public void init() {
                  Mockito.reset(logger); // important as it's static
                  // ...
          
              @Test
              public void myTest() throws Exception {
                  // ...
                  verifyNoInteractions(logger);
              }
          }
          

          【讨论】:

            猜你喜欢
            • 2014-02-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-07-19
            • 1970-01-01
            • 2015-11-04
            • 2019-05-31
            相关资源
            最近更新 更多