66

我有以下要模拟的 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. 我似乎只能在其中运行when(loggerMock.isDebugEnabled()).thenReturn(true);@Before因此我似乎无法更改每种方法的特征。有没有解决的办法?

编辑结果:

我以为我已经尝试过了,但没有奏效:

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

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

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

我得到零互动。我也试过PowerMockito.verifyStatic();但@Before也有零相互作用。

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

4

8 回答 8

74

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

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

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

于 2012-01-29T11:44:33.580 回答
19

聚会有点晚了——我正在做类似的事情,需要一些指导,最后到了这里。没有功劳——我从 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 个调用这个的。这是漂亮的裤子,但我找不到清除调用的方法。我想知道是否有人能想到解决这个问题的方法......

于 2014-06-11T20:57:37.973 回答
6

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

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

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

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

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

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

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

于 2012-01-21T00:36:36.910 回答
2

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

于 2014-11-05T12:06:50.560 回答
1
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.
  }
 
}
于 2021-06-10T16:13:50.633 回答
1

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

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

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

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

所以...

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

于 2015-11-30T19:08:48.253 回答
0

以下是一个模拟类中命名log的私有静态最终记录器的测试类LogUtil

除了模拟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();
    }
} 
于 2021-01-21T09:05:32.687 回答
0

我只需要一项测试就可以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);
    }
}
于 2021-09-10T11:00:35.807 回答