Mockito无法验证来自org.slf4j.Logger的多个方法调用

时间:2013-11-25 14:56:07

标签: java unit-testing logging mockito powermock

我有一个包含2个条件的方法。在每个条件中,都会调用Logger.error方法。验证该方法调用的第一个测试成功,但任何其他测试都失败

  

通缉但没有被援引......实际上,与之互动是零   这个模拟。

有谁知道为什么会这样?

下面,我提供了一个示例类和一个单元测试,它将产生问题:

package packageName;

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

public class X {

    private static final Logger LOGGER = LoggerFactory.getLogger(X.class);

    public void execute(boolean handle1stCase) {
        if (handle1stCase) {
            LOGGER.error("rumpampam");
        } else {
            LOGGER.error("latida");
        }
    }
}

测试:

package packageName;

import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.powermock.api.mockito.PowerMockito;
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.Mockito.*;
import static org.powermock.api.mockito.PowerMockito.mockStatic;

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

    @Mock
    private Logger loggerMock;

    private X x;

    @Before
    public void construct() {
        MockitoAnnotations.initMocks(this);

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

        x = new X();
    }

    @Test
    public void whenFirstCaseErrorLogged() throws Exception {
        x.execute(true);
        verify(loggerMock, times(1)).error("rumpampam");
    }

    @Test
    public void whenSecondCaseErrorLogged() throws Exception {
        x.execute(false);
        verify(loggerMock, times(1)).error("latida");
    }
}

结果:

  

想要但未被调用:loggerMock.error(“latida”);    - > at packageName.XTest.whenSecondCaseErrorLogged(XTest.java:51)
  实际上,这个模拟没有互动。

修改
我简短地回答了为什么除了第一个测试之外的每个测试都在comment of this answer失败了。

我对问题的解决方案
在测试中提供:

public static Logger loggerMockStatic;  

比只为所有测试创建一个实例并在静态变量中提供它,并使用静态loggerMockStatic而不是on。所以你会:

    ...  
    MockitoAnnotations.initMocks(this);

    if (loggerMockStatic == null) {
        loggerMockStatic = loggerMock;
    }

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

并在验证方法中使用loggerMockStatic而不是loggerMock。

关于方法的一些想法
对我来说这很好,因为
1.它不破坏设计(如果你认为所需的变量应该是一个常数,那么它将保持这种方式) 2.它在测试中只添加了4行,允许您测试常量(在本例中为记录器)行为。污染程度不大,测试用例仍然很清楚。

我在this answer中解释的“删除最终并提供setter”方法可以打开系统漏洞。没有人需要将记录器设置为类,我总是希望系统根据需要打开。不需要仅为需要测试而提供设定器。测试应该适用于实现,而不是相反。

特别是在测试日志记录时,我不认为应该在一般(大多数)情况下测试日志记录。记录应该是应用程序的一个方面。当你有其他输出来测试某个路径时,应测试那些输出。但是在这种情况下(也许是其他情况),某个路径没有其他输出,例如在特定条件下记录和返回,需要测试日志(根据我)。我想知道即使有人改变了条件,日志消息仍将被记录。如果没有日志,并且如果有人以错误的方式更改条件,则无法知道错误存在于这段代码中(除非可能通过调试)。

我正和一些同事讨论过,有一个单独的类来进行日志记录就可以了。这样,常量在另一个类中被隔离,并且您将能够仅使用Mockito检查行为。他们进一步说,如果您想将日志发送到电子邮件,这将更容易更改 首先,我认为这是一种过早的模块化,如果您不打算在不久的将来在记录方式之间切换 其次,只使用Mockito +有另一个类和+3行代码VS我的一行代码(logger.error(...))+使用PowerMockito,我会再次使用后者。在测试期间添加其他依赖项不会使您的生产代码变得更慢,更笨重。也许在考虑继续集成并且测试也和其他阶段一样重要时,您可能会说这会使测试过程变得更慢更笨重,但我会牺牲它 - 对我来说似乎没什么大不了的。

3 个答案:

答案 0 :(得分:4)

您的记录器是静态的,因此在加载类时不会在初始化对象时加载它。你没有担心你的模拟会按时准备好,有时它有时可能不起作用。

答案 1 :(得分:1)

这就是为什么这不起作用:

X类中的字段是static和final,只允许在第一次加载类时设置它。由于我在第一个答案中所写的内容,这很危险。在你的情况下,你很幸运,这不会发生,但......

Junit按以下顺序执行测试用例: 构造() whenFirstCaseErrorLogged() 构造() whenSecondCaseErrorLogged()

现在假设在第一次调用construct()之后,XTest的字段loggerMock指向驻留在地址0001的对象。然后,LoggerFactory使用该对象初始化x对象的LOGGER字段。然后从whenFirstCaseErrorLogged()调用x.error并使用finde,因为loggerMock和X :: Logger都指向同一个对象。

现在我们到达第二个构造()。你的loggerMock被重新初始化,现在它指向一个不同的对象,假设它存储在地址0002的内存中。这是一个与先前创建的不同的新对象。现在因为你的X :: LOGGER是静态的,所以它不会被重新初始化,因此它仍然指向存储在地址0001的对象。当你试图验证在loggerMock上调用的方法时,你会得到错误,因为在该对象上没有执行任何操作而是调用前一个对象的错误方法。

以下是我的一些想法。也许他们会显得有帮助。 我想在将来你应该重新考虑使用静态两次。为什么你想在不恒定时做一些事情?在第二次运行后,您的参考变量是否具有相同的值?当然可能会发生,但这种可能性很小。静态最终可以阻止你改变对象的状态吗?当然不是它们只会阻止您将LOGGER重新分配给其他实例。您在之前的评论中提到,您不希望代码的用户为您的LOGGER提供空引用。没关系,但你可以通过在提供异常或使用不同的空处理机制时抛出异常来防止这种情况。

关于使用static关键字已经说了很多。有些人认为它是纯粹的邪恶有些人没有,有些人仍然喜欢单身人士:)。

无论你怎么想,你必须知道静态对测试和线程没有好处。 当像PI或euler数字那样是静态的时候我使用静态final,但是对于具有可变状态的对象我不使用static final。我对实用程序类使用静态方法,这些实用程序类不存储状态,只是进行一些处理(解析,计数等)并返回结果imediatelly。一个很好的例子是像电力这样的数学函数。

我认为这会很有用;)

答案 2 :(得分:0)

向类X添加方法以允许设置记录器,并从中删除final。然后在测试中做这样的事情。

@Mock private Logger mockLogger;
private X toTest = new X();

...
@Before
public void setUp() throws Exception {
    toTest.setLogger(mockLogger);
}

@Test
public void logsRumpampamForFirstCall() throws Exception {
    toTest.execute(true);
    verify(mockLogger).error("rumpampam");
}

@Test
public void logsLatidaForOtherCalls() throws Exception {
    toTest.execute(false);
    verify(mockLogger).error("latida");
}