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

Unit test failure on testing a code involving ScheduledExecutorService#scheduleAtFixedRate

对于可重现的例子,我有以下 class

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

我说了,半不正确,因为如果我增加实际代码中的初始延迟来举例说明,这个测试就会通过1 MINUTE

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

@Test
public void testInvokingStart() throws Exception {
    sampleCaching.start();
    verify(interestCache, times(1)).populateOne();
    verify(interestCache, times(1)).populateTwo();
}

它总是成功执行,但另一方面,为多媒体添加 verify 测试总是失败:

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

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

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

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