【问题标题】:Mockito fails to verify multiple invocations of methods from org.slf4j.LoggerMockito 无法验证来自 org.slf4j.Logger 的方法的多次调用
【发布时间】:2013-11-25 14:56:06
【问题描述】:

我有一个包含 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"); -> 在 packageName.XTest.whenSecondCaseErrorLogged(XTest.java:51)
实际上,与此模拟的交互为零。

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

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

public static Logger loggerMockStatic;  

比只为所有测试创建一个实例并在静态变量中提供它,然后使用静态 loggerMockStatic 从比开始。所以你会有:

    ...  
    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 中解释的“删除final 并提供setter”方法会使系统容易受到攻击。不需要有人将记录器设置为类,我总是希望系统根据需要打开。不希望只为测试需要提供设置器。测试应该适用于实现,而不是相反。

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

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

【问题讨论】:

    标签: java unit-testing logging mockito powermock


    【解决方案1】:

    您的记录器是静态的,因此它在您的类加载时加载,而不是在对象初始化时加载。你不能保证你的模拟会准时准备好,有时它可能会工作,有时可能不会。

    【讨论】:

    • 在这种情况下,第一次测试有时会失败。但事实并非如此!
    • 因为你很幸运,你的第一个测试总是先执行:)
    • 我希望这种幸福跟随我 :) 但是第一个测试用例失败从未发生过。做我的客人 :) 尝试 1000 次,你不会让它失败。这就是原因(没有时间回复)。在第一次测试中,powermockito 指示静态 getLogger 方法返回 loggerMock',第一次测试将始终成功。我的假设是类被重新加载,因此当在第二次测试中 powermockito 指示 getLogger 方法返回 loggerMock'' 时,第二次测试将成功。但是第二个测试使用 loggerMock' 代替。这就是为什么其他所有测试都失败的原因。我在编辑中的解决方案
    【解决方案2】:

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

    类 X 中的字段是静态的和最终的,它只允许在第一次加载类时设置它。由于我在第一个答案中写的内容,这很危险。在你的情况下,你很幸运,这并没有发生,但是......

    Junit 按以下顺序执行您的测试用例: 构造() 当FirstCaseErrorLogged() 构造() whenSecondCaseErrorLogged()

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

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

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

    关于使用 static 关键字的说法很多。有些人认为这是纯粹的邪恶,有些人不认为,有些人仍然喜欢单身:)

    无论您认为什么,您都必须知道静态对测试和线程没有好处。 当诸如 PI 或欧拉数之类的静态事物时,我使用静态 final,但对于具有可变状态的对象,我不使用静态 final。我对不存储状态但只进行一些处理(解析、计数等)并立即返回结果的实用程序类使用静态方法。一个很好的例子是像幂这样的数学函数。

    我认为这会很有用;)

    【讨论】:

    • “在你的情况下,你很幸运,这并没有发生,但是......” 关于运气,这不是真的。我在评论您的其他答案时解释了确切的原因:stackoverflow.com/a/20196591/521754 我建议将来不要写多个答案,而是使用已编辑的评论来编辑您的旧答案。
    • “为什么你想让一些东西不是恒定的?”记录器不是常数?!我可能错了,但它不应该在运行时改变。这不是java中常量的定义吗?! :) “静态最终能阻止你改变对象的状态吗?”你会在记录器中改变什么状态我只是好奇:)(检查 org.slf4j.Logger 接口,看看它有什么方法;))
    • 没有必要在你的答案中涉及内存地址 :) 这已经足够了:“因为你的 X::LOGGER 是静态最终的,它不会被重新初始化”;) 无论如何,我我将接受您的回答,因为以“现在让我们说...”开头的 2 段与我在您之前的回答 stackoverflow.com/a/20196591/521754 中给您的评论相同(我可以看到您应该得到提升 :)) .感谢您的努力,卢克!我将为任何感兴趣的人提供我如何处理这种情况的具体方法。
    • 很高兴我们在您的代码未给出预期结果的原因方面意见一致。正如您提到的 org.slf4j.Logger 是一个接口,因此您无法更改它的状态,但实现它的类可以将记录的消息存储在向量字段或某事物中,这就是我所指的状态。我关于将 final static 与 class' 字段一起使用的评论的目的是表明它最终不会给你买任何东西(你可以用不同的方式防范空值),而是给测试带来了复杂性(你在你不是吗;))
    • 查看我所做的最后一次编辑。我希望你会同意它毕竟很容易解决(=> 无需更改设计)。干杯
    【解决方案3】:

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

    【讨论】:

    • 这将使测试可能工作。不幸的是,我绝不会为了取悦测试而改变系统的设计。 LOGGER 变量故意是一个常量(这样其他人就无法在整个应用程序中更改它)。
    • 这对我来说似乎是一个奇怪的选择。此更改将使代码更具可测试性。更可测试的代码较少受到错误的影响。事实上,易于测试是代码可维护性的重要组成部分。
    • @David Wallace 是对的,但在这种情况下,我们可以在不更改系统的情况下进行测试 :)
    • @DavidWallace 是的,代码应该是可测试的,但我无法想象不使用常量,因为我想测试代码。这就是发明 PowerMockito 等框架的原因。 MariuszS,不是“可以”而是“必须”。否则,您将系统打开漏洞。无论如何,我仍然不明白为什么 mockStatic 和 verify 不能正确地做到这一点:(
    • 我不是说“不要使用常量”。我是说“不要在这种情况下使用它们”。编写可测试的代码是一门学科,随着时间的推移,你会从中受益。或者更有可能的是,下一个 使用此代码的人将获得好处。 PowerMock 的存在是那些尚未学习这门学科的人的拐杖。
    猜你喜欢
    • 2013-01-31
    • 1970-01-01
    • 2018-06-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多