使用ScheduledExecutorService测试代码(无需使用Sleep)

13

我有一个验证对象,会将输入内容通过一系列的检查。如果任何一个输入未通过检查,验证结束。

通过所有检查的输入会基于一个滑动时间窗口进行分组。该窗口在第一个输入到达时启动。所以流程如下:

  1. 第一个输入到达。
  2. 输入通过所有检查。
  3. 由于没有活动计时器,将输入放入新的篮子中。N秒计时器窗口开始。
  4. 在这个计时器窗口内通过所有检查的后续输入将被分组到同一个篮子中。
  5. 计时器结束后,篮子将被调度。
  6. 任何进一步有效的输入都会启动一个新的计时器,并重复该过程。

目前为止,为确保有效输入正确分组,我在单元测试中使用Thread.sleep(即,一旦我发送了若干输入,我会睡眠几秒钟,然后唤醒并确保已调度篮子包含所有预期内容)。

随着我有超过700个单元测试,这个测试集合每次运行完整套件都成为瓶颈,这变得非常烦人。

时间窗口只是ScheduledExecutorService。为了更快地测试此功能,我是否应该创建一个可设置时间窗口对象?


你可以考虑给你的问题添加一个或两个额外的标签,以便将其针对到适当的受众。例如,你正在使用哪种编程语言,你的单元测试框架是什么等等。为什么要浪费时间给你提供不合适的答案呢? - Sunil D.
进一步说,对于涉及AngularJs中计时器/休眠的测试,有一个不错的解决方案。但显然这不是你正在使用的 :) - Sunil D.
谢谢你的提示。已完成。 - Mark Cuban
5个回答

15

你的“单元测试”听起来有点像“集成测试”。你不仅测试使用ScheduledExecutorService的单元,还要测试ScheduledExecutorService本身。

更好的方法是注入一个 mock ScheduledExecutorService。换句话说,你不需要测试定时事件是否实际在四秒后发生; 你只需要测试你的单元是否要求调度器在四秒后运行它。

这就是mock的作用。你注入 mock 调度器,在单元上执行一些操作,应该会导致它与调度器交互,然后你可以询问mock以验证交互是否按预期方式发生。

如果做得正确,每个测试用例可以在毫秒甚至微秒内完成,而不是几秒钟。


4

我发现,DeterministicScheduler (来自于 jMock 库) 是测试使用 ScheduledExecutorService 的代码的好方法。

它提供了类似于 TestScheduler 为使用 RxJava 的代码和 DelayController 为使用协程的 Kotlin 代码提供的功能。

在这两种情况下,tick() 做的正是前面提到的库中 advanceTimeBy() 所做的事情: 它推进了虚拟时间并运行任何需要在给定 时间范围内 执行的任务。

您需要添加 jMock 核心库才能使用它。

例如,使用 Gradle:


dependencies {
    //Used only as provider of DeterministicScheduler (test implementation of ScheduledExecutorService)
    testImplementation("org.jmock:jmock:2.12.0")
}

离题:据我所见,这是一般用途的功能,与jMock的模拟功能无关。 理想情况下,最好提供它作为一个单独的JAR/maven artifact,以便人们可以轻松地拉取它,而不必添加整个jmock库。 我已经创建了一个问题,提出了这个建议。


2
很难使ScheduleExecutorService本身可测试。令人沮丧的是,它的实现(ScheduledThreadPoolExecutor)确实有一个now()方法。原则上,您可以覆盖它并控制时间!问题在于该方法是包私有和final的,因此无法被覆盖。可能可以使用类似PowerMock的东西来覆盖它。
如果无法覆盖此方法,则没有太多ScheduleExecutorService可用。原则上,您可以实现自己的ThreadPoolExecutor子类,以遵守ScheduledExectuorService,但这涉及实现自定义BlockingQueue,这是一件非常复杂的事情。
最简单的方法是使用ScheduleExecturoService的模拟实现-在Mockito Wiki上有一个(不完整)的例子可以做到这一点。

1
我已经为测试实现了一个FixedClockScheduledExecutorService,希望对某些人有所帮助。使用它很简单,只需像往常一样安排任务,并在需要推进时间并使事情发生时调用elapse()即可。
(有几个小警告:排序未实现,关闭逻辑也未实现,而且可能不适用于极端时间范围。如果必要,这些都可以修复)。
class FixedClockScheduledExecutorService extends AbstractExecutorService implements ScheduledExecutorService {
    public FixedClockScheduledExecutorService() {}

    private final Collection<Job<?>> jobs = new CopyOnWriteArrayList<>(); //Collection must support concurrent modification. TODO: Needs ordering
    private long offsetNanos = 0;

    //Call this to advance the clock...
    public void elapse(long time, TimeUnit timeUnit) {
        offsetNanos += NANOSECONDS.convert(time, timeUnit);

        for(Job<?> job: jobs) {
            if(offsetNanos >= job.initialDelayNanos) {
                jobs.remove(job);
                job.run();
            }
        }
    }

    private <V> ScheduledFuture<V> scheduleIntenal(Callable<V> callable, long delay, long period, TimeUnit timeUnit) {
        Job<V> job = new Job<V>(callable, offsetNanos + NANOSECONDS.convert( delay, timeUnit), NANOSECONDS.convert(period, timeUnit));
        jobs.add(job);
        return job;
    }


    @Override
    public ScheduledFuture<?> schedule(Runnable runnable, long delay, TimeUnit timeUnit) {
        return schedule(Executors.callable(runnable, null), delay, timeUnit);
    }

    @Override
    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit timeUnit) {
        return scheduleIntenal(callable, delay, 0, timeUnit);
    }

    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable runnable, long delay, long period, TimeUnit timeUnit) {
        return scheduleIntenal(Executors.callable(runnable, null), delay, Math.abs(period), timeUnit);
    }

    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable runnable, long delay, long period, TimeUnit timeUnit) {
        return scheduleIntenal(Executors.callable(runnable, null), delay, -Math.abs(period), timeUnit);
    }


    class Job<V> extends FutureTask<V> implements ScheduledFuture<V> {
        final Callable<V> task;
        final long initialDelayNanos;
        final long periodNanos;

        public Job(Callable<V> runner, long initialDelayNanos, long periodNanos) {
            super(runner);
            this.task = runner;
            this.initialDelayNanos = initialDelayNanos;
            this.periodNanos = periodNanos;
        }
        @Override public long getDelay(TimeUnit timeUnit) {return timeUnit.convert(initialDelayNanos, NANOSECONDS);}
        @Override public int compareTo(Delayed delayed) {throw new RuntimeException();} //Need to implement this to fix ordering.

        @Override public void run() {
            if(periodNanos == 0) {
                super.run();
            } else {
                //If this task is periodic and it runs ok, then reschedule it.
                if(super.runAndReset()) {
                   jobs.add(reschedule(offsetNanos));
                }
            }
        }

        private Job<V> reschedule(long offset) {
            if(periodNanos < 0) return new Job<V>(task, offset, periodNanos); //fixed delay
            long newDelay = initialDelayNanos;  while(newDelay <= offset) newDelay += periodNanos; //fixed rate
            return new Job<V>(task, newDelay, periodNanos);
        }
    }

    @Override public void execute(Runnable command) { schedule(command, 0, NANOSECONDS); }
    @Override public void shutdown() {}
    @Override public List<Runnable> shutdownNow() { throw new RuntimeException(); }
    @Override public boolean isShutdown() { return false;}
    @Override public boolean isTerminated() { return false;}
    @Override public boolean awaitTermination(long timeout, TimeUnit unit) { return true; }
}

这真的太棒了。 - omri

0
如果您正在使用 Kotlin 和 mockk,则模拟的 ScheduledExecutorService 是正确的选择。
class MyExecutorClass() {
    fun executeSomething() {
        Executors.newSingleThreadScheduledExecutor().schedule( {
                // do something here
            }, 3, TimeUnit.SECONDS)
    }
}
       
@Test
fun myExecutorTest() {
    // setup
    val capturingSlotRunnable: CapturingSlot<Runnable> = slot()
    mockkStatic(Executors::class)
    val mockExecutor: ScheduledExecutorService = mockk(relaxed = true)
    every { Executors.newSingleThreadScheduledExecutor() } returns mockExecutor
    every { mockExecutor.schedule(capture(capturingSlotRunnable), any(), any()) } returns null
    
    // execute
    val myExecutor = MyExecutorClass()
    myExecutor.executeSomething()
    val runnable = capturingSlotRunnable.captured
    runnable.run()

    // verify
    // TODO: verify the executable block did whatever it was supposed to
}

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