Mockito: 使用mockStatic() - 如何在作用域模拟对象上模拟实例方法

3
这是一个简单的测试示例,展示了什么时候以及为什么会失败。简而言之,我模拟了Instance类的静态now()方法,但后来又调用了这个被模拟的实例的另一个方法-minus(),它将返回null
@Test
    void givenInstantMock_whenNow_thenGetFixedInstant() {
        String instantExpected = "2014-12-22T10:15:30Z";
        Clock clock = Clock.fixed(Instant.parse(instantExpected), ZoneId.of("UTC"));
        Instant instant = Instant.now(clock);

        try (MockedStatic<Instant> mockedStatic = mockStatic(Instant.class)) {
            mockedStatic.when(Instant::now).thenReturn(instant);
            Instant now = Instant.now();
            assertThat(now.toString()).isEqualTo(instantExpected);

            Instant earlier2weeks = now.minus(Period.ofWeeks(2));
            assertNotNull(earlier2weeks);// FAILS as it will be null
        }
    }

在实际情况中,服务类使用Instant::now创建一个新的Instant。
//in a component class
@Component
class DeletionScheduler {
...
public void findOutdatedEntries() {
  Instant limitDate = Instant.now().minus(validityGracePeriod);
        List<Entry> entriedToDelete= shareService.findOutdatedEtriees(limitDate );
...
}

所以你可以看到,即使我嘲笑Instant.now(),对这个实例调用minus总是会返回NULL。
So the idea of the test was:

- to create an entry with a `limitDate` in the future.
- trick/mock the System clock to be in the future
- check if there are some outdated entries.

Is there any way to get around it?
 

1
将Clock视为服务,注入它,并使用Clock.now()或Instant.now(Clock)获取当前时间。然后在测试中,您可以注入所需的Clock.fixed(...)。 - bowmore
1
将时钟视为一个服务,注入它,并使用Clock.now()或Instant.now(Clock)来获取当前时间。在测试中,您可以注入所需的Clock.fixed(...)。 - bowmore
1
将Clock视为一个服务,注入它,并使用Clock.now()或Instant.now(Clock)来获取当前时间。然后在测试中,你可以注入你需要的Clock.fixed(...)。 - undefined
非常感谢您的建议。我已经更新了原始评论内容,以便更清楚地理解。 - belgoros
非常感谢您的建议。我已经更新了原始评论内容,以便更清楚地理解。 - belgoros
非常感谢您的建议。我已经更新了原始评论的内容,以便更清楚地理解。 - undefined
2个回答

2

解释

这是因为在try块内,Instant的所有静态方法都返回null,行为与我们对实例方法进行实例模拟时相同。

如果我们调试进入minus调用,最终会到达ofEpochSecond,它将返回null。

    private Instant plus(long secondsToAdd, long nanosToAdd) {
        if ((secondsToAdd | nanosToAdd) == 0) {
            return this;
        }
        long epochSec = Math.addExact(seconds, secondsToAdd);
        epochSec = Math.addExact(epochSec, nanosToAdd / NANOS_PER_SECOND);
        nanosToAdd = nanosToAdd % NANOS_PER_SECOND;
        long nanoAdjustment = nanos + nanosToAdd;  // safe int+NANOS_PER_SECOND
        return ofEpochSecond(epochSec, nanoAdjustment);
    }

解决方法
你可以为defaultAnswer提供设置,以默认调用真实方法。
try (MockedStatic<Instant> mockedStatic = mockStatic(Instant.class,  
     withSettings().defaultAnswer(invocation -> invocation.callRealMethod()))) {
     mockedStatic.when(Instant::now).thenReturn(instant);
     ...
}

更好的解决方案

参考Clock javadoc

这个抽象的主要目的是允许在需要时插入替代时钟。应用程序使用一个对象来获取当前时间,而不是静态方法。这可以简化测试。

并且根据bowmore的建议,我们应该将Clock视为一个服务,并根据需要设置时间。

此外,静态方法默认是全局的,因此更改(静态模拟)它的行为与更改全局变量一样容易出错。


使用 withSettings().defaultAnswer(invocation -&gt; invocation.callRealMethod() 对我很有效。非常感谢!!! - belgoros
使用withSettings().defaultAnswer(invocation -> invocation.callRealMethod()对我来说效果很好。非常感谢!!! - belgoros
使用withSettings().defaultAnswer(invocation -> invocation.callRealMethod()对我来说效果很好。非常感谢! - undefined

0

你需要在Mockito中使用Mocked Construction:

尝试代码块:

try (MockedConstruction<Instant> mockedConstruction = mockConstruction(Instant.class,
            (mock, context) -> {
                //this initializer will be called each time during mock creation
                when(mock::now).thenReturn(instant);
            })) {
        
        Instant now = Instant.now();
        assertThat(now.toString()).isEqualTo(instantExpected);

        Instant earlier2weeks = now.minus(Period.ofWeeks(2));
        assertNotNull(earlier2weeks);// should work now
    }

以上代码应该可以正常工作,因为它会模拟对象的每一个新建构造

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接