使用PowerMock和Mockito模拟Logger和LoggerFactory

时间:2012-01-20 22:57:47

标签: java junit mockito slf4j powermock

我有以下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,因此我似乎无法更改每种方法的特征。有办法解决这个问题吗?

  3. 编辑调查结果:

    我以为我已经尝试了这个并且它没有用:

     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确实如此,但两人都同意验证失败。

7 个答案:

答案 0 :(得分:61)

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

对于这个问题,我建议使用ReporterreportIncorrectUseOfYAndZForActionX等方法制作类似= reportProgressStartedForActionX类的内容。这样做的好处是可以让任何人阅读代码都能看到该功能。但它也有助于实现测试,更改此特定功能的实现细节。

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

答案 1 :(得分:13)

派对有点晚了 - 我做了类似的事情,需要一些指针,最后到此为止。没有信用 - 我从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"); - 而不是每个测试中的时间(1)作为第二个测试会失败说那里有2个调用。这是很好的裤子,但我找不到清除调用的方法。我想知道是否有人能想到这个方法......

答案 2 :(得分:5)

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

   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对象设置了logger变量。

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

答案 3 :(得分:2)

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

答案 4 :(得分:0)

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

使用类似静态日志绑定器的任何类加载器或者与环境混乱的模式都认为像logback.XML在测试时会破产。

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

并非所有关于单元测试的。有时我们希望集成测试发出日志记录。有时我们不会。我们希望有人能够有选择地抑制某些集成测试日志记录,例如,预期的错误可能会使CI控制台混乱并造成混淆。如果你从主线的顶部(或者你可能使用的任何di框架)注入ILoggerFactory,那么一切都很简单

因此...

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

答案 5 :(得分:0)

以下是模拟类 log 中名为 LogUtil 的私有静态最终 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();
    }
} 

答案 6 :(得分:0)

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