【问题标题】:Testing code which uses ScheduledExecutorService (without using Sleep)测试使用 ScheduledExecutorService 的代码(不使用睡眠)
【发布时间】:2015-09-08 13:30:41
【问题描述】:

我有一个验证对象,它通过一系列检查运行输入。如果输入未通过任何检查,则验证结束。

通过所有检查的输入将根据滑动时间窗口进行分组。当第一条输入到达时,此窗口启动。所以这是流程:

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

目前,为了确保有效输入正确组合在一起,我在单元测试中使用 Thread.sleep(即,一旦我发送了许多输入,我会休眠几秒钟,然后醒来并进行确保发送的篮子包含预期的所有内容)。

这开始变得很烦人,因为我有超过 700 个单元测试,而这个测试集合是我每次运行完整套件时的瓶颈。

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

【问题讨论】:

  • 您可能会考虑在您的问题中添加一两个额外的标签,以便将其定位到适当的受众。例如。你用什么语言编码,你的单元测试框架是什么等等。我们为什么要浪费时间给你不恰当的答案。
  • 进一步我的评论,对于涉及计时器/在 AngularJs 中休眠的东西的测试,有一个很好的解决方案。但这显然不是你使用的:)
  • 感谢您的提示。完成。

标签: java multithreading unit-testing junit scheduledexecutorservice


【解决方案1】:

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

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

这就是模拟的用武之地。您注入模拟调度程序,在您的单元上执行一些操作,使其与调度程序交互,然后您可以询问模拟以验证交互是否以预期的方式实际发生。

如果你做对了,每个测试用例可以在毫秒或微秒内完成,而不是几秒。

【讨论】:

    【解决方案2】:

    我发现,DeterministicScheduler (来自jMock lib) 是测试代码的好方法,它使用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")
    }
    

    题外话:据我所知,它是通用的 purouse 功能,与 jMock 模拟功能无关。 理想情况下,最好将它作为单独的 JAR/maven 工件提供,这样人们就可以轻松地提取它而无需添加整个 jmock 库。 我已经用这个建议添加了an issue

    【讨论】:

      【解决方案3】:

      ScheduleExecutorService 本身可测试非常困难。令人沮丧的是,它的实现(ScheduledThreadPoolExecutor)确实有一个now() 方法。原则上你可以覆盖它并控制时间!问题是该方法是包私有的和最终的,所以它不能被覆盖。可以使用 PowerMock 之类的东西来覆盖它。

      如果您不能覆盖此方法,那么您可以使用的ScheduleExecutorService 并不多。原则上,您可以实现自己的ThreadPoolExecutor 子类,它尊重ScheduledExectuorService,但这涉及实现自定义BlockingQueue,这是一件非常复杂的事情。

      最简单的方法是使用 ScheduleExecturoService 的 Mock 实现 - 在 Mockito Wiki 上有一个(不完整的)示例正是这样做的。

      【讨论】:

        【解决方案4】:

        我已经实现了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; }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多