Java 测试涉及ScheduledExecutorService#scheduleAtFixedRate的代码时单元测试失败

Java 测试涉及ScheduledExecutorService#scheduleAtFixedRate的代码时单元测试失败,java,unit-testing,junit,mockito,scheduledexecutorservice,Java,Unit Testing,Junit,Mockito,Scheduledexecutorservice,关于可复制的例子,我有以下的类 public class SampleCaching { ScheduledExecutorService executorService; @com.google.inject.Inject InterestCache interestCache; @Inject MultimediaCache multimediaCache; @Inject public SampleCaching(InterestCache in

关于可复制的例子,我有以下的类

public class SampleCaching {

    ScheduledExecutorService executorService;
    @com.google.inject.Inject InterestCache interestCache;
    @Inject MultimediaCache multimediaCache;

    @Inject
    public SampleCaching(InterestCache interestCache, MultimediaCache multimediaCache) {
        this.executorService = Executors.newScheduledThreadPool(3);
        this.interestCache = interestCache;
        this.multimediaCache = multimediaCache;
    }

    protected void calculate() {
        interestCache.populateOne();
        interestCache.populateTwo();
        multimediaCache.populateMultimedia();
        log.info("Cache population completed!");
    }

    public void start() {
        executorService.scheduleAtFixedRate(this::calculate, 
                                0, 20, TimeUnit.MINUTES); // notice initial delay 
    }
}
事实上,我为这段代码编写了一个半错误的单元测试,内容如下:

@org.junit.runner.RunWith(PowerMockRunner.class)
@org.powermock.core.classloader.annotations.PowerMockIgnore("javax.management.*")
public class SampleCachingTest {

    @org.mockito.Mock InterestCache interestCache;
    @Mock MultimediaCache multimediaCache;
    @org.mockito.InjectMocks SampleCaching sampleCaching;

    @Test
    public void testInvokingStart() throws Exception {
        sampleCaching.start();
        verify(multimediaCache, times(0)).populateMultimedia();
        verify(interestCache, times(0)).populateOne();
        verify(interestCache, times(0)).populateTwo();
    }
}
  • 我看了一遍,但我不知道出于什么原因,我需要在这里模拟
    scheduleAtFixedRate
    的返回类型,不管怎样,模拟对我来说工作正常
  • 找到了与
    ScheduledExecutorService
    测试关系不大的最上等答案(至少我现在是这么想的)
我说,这是半错误的,因为如果我增加实际代码中的初始延迟,例如
1分钟,这个测试就通过了

真正让我问这个问题的是如果我把测试改为

@Test
public void testInvokingStart() throws Exception {
    sampleCaching.start();
    verify(interestCache, times(1)).populateOne();
    verify(interestCache, times(1)).populateTwo();
}
它总是成功执行,但添加
验证
用于多媒体时,测试总是失败,另一方面:

verify(multimediaCache, times(1)).populateMultimedia(); // or even to `times(0)`

这种行为(确定性或确定性)背后有什么原因吗?修复此测试的正确方法是什么?

因此,您正在触发方法SampleCaching#启动自己,这反过来会通知ScheduledExecutorService调用calculate方法,初始延迟为0秒。这将在一个单独的线程中发生。同时,测试代码将继续运行,接下来要做的是验证在多媒体缓存上没有调用populateMultimedia方法。然后对populateOne和populateTwo也一样。 此操作的成功与否取决于启动的另一个线程中计算方法的进度。如果它已经调用了populateMultimedia方法,那么您的第一次验证将失败,其他验证也将失败。另一方面,如果它没有进展到那么远,测试将成功,但可能在populateOne或populateTwo上失败

您或者需要构建一个同步机制(例如java.util.concurrent.CountDownLatch),这个计算方法在最后进行倒计时,测试代码在验证之前进行等待,或者在调用start方法和验证调用之间设置一个合理的延迟。
第一种是侵入性的,因为它会更改您正在测试的组件。你可以考虑创建一个SimuleCurp的子类,它覆盖了计算方法,但是如果你的计算方法是私有的,那么它又是侵入性的。

谢谢你回答这个问题,关于“比你的第一个验证失败,其他人也会失败……”,有一个陷阱,尽管我在实际代码中更改了方法调用的顺序或在测试中进行了验证,但测试始终无法验证
populateMultimedia
方法。(我使用IntelliJ来执行这些测试,但在测试验证期间如何执行这些方法会有影响。)…除此之外,这是观察到的一个奇怪的点,并促使我将此作为一个问题提出。更改实际代码中的顺序或更改验证顺序不会改变此测试的基本问题。测试无法忽略两个并发线程正在执行的事实。一个执行逻辑,一个验证逻辑是否正在执行,这一切都取决于线程上执行的时间。使用CountDownLatch将对您有所帮助。请注意,时间(0)可以替换为never(),而时间(1)可以始终忽略。因此,verify(multimediaCache).populateMultimedia()等于verify(multimediaCache,乘以(1)),populateMultimedia()