1. 概述

一个ExecutorService对象在后台运行任务。对在其他线程上运行的任务进行单元测试具有挑战性。主线程必须等待任务结束才能验证其结果

此外,解决这个问题的一个方法是使用Thread.sleep()方法。这个方法会让主线程阻塞一段时间。然而,如果任务超过了sleep()设置的时间,单元测试会在任务完成之前结束并失败。

在这篇教程中,我们将学习如何不使用Thread.sleep()方法来单元测试ExecutorService实例。

2. 创建Runnable对象

在深入测试之前,让我们创建一个实现Runnable接口的类:

public class MyRunnable implements Runnable {
    Long result;

    public Long getResult() {
        return result;
    }

    public void setResult(Long result) {
        this.result = result;
    }

    @Override
    public void run() {
        result = sum();
    }

    private Long sum() {
        Long result = 0L;
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            result += i;
        }
        return result;
    }
}

MyRunnable类执行一个耗时很长的计算,然后将计算结果设置为result成员字段。因此,这将是我们将提交给执行器的任务。

3. 问题

通常,一个ExecutorService对象在一个后台线程中运行任务。任务实现了CallableRunnable接口。

如果主线程没有等待,它会在任务完成前终止。所以,测试总是失败。

让我们创建一个单元测试来验证这个问题:

ExecutorService executorService = Executors.newSingleThreadExecutor();
MyRunnable r = new MyRunnable();
executorService.submit(r);
assertNull(r.getResult());

在这个测试中,我们首先创建了一个单线程的ExecutorService实例。然后,我们创建并提交了任务。最后,我们断言了result字段的值。

在运行时,断言发生在任务结束之前。所以,getResult()返回null

4. 使用Future

Future类表示后台任务的结果。而且,它可以阻止主线程直到任务完成

让我们修改测试以使用submit()方法返回的Future对象:

Future<?> future = executorService.submit(r);
future.get();
assertEquals(2305843005992468481L, r.getResult());

这里,**Future实例的get()方法会阻塞直到任务结束**。

另外,如果任务是Callable的实例,get()方法可能会返回一个值。**如果任务是Runnable的实例,get()始终返回null**。

现在运行测试比以前花费更长的时间。这是主线程正在等待任务完成的迹象。最后,测试成功了。

5. 关闭并等待

另一种选择是使用ExecutorService类的shutdown()awaitTermination()方法

shutdown()方法关闭执行器。执行器不再接受新任务,但不会杀死现有的任务。然而,它不会等待它们结束。

另一方面,我们可以使用**awaitTermination()方法阻塞直到提交的所有任务结束**。此外,我们应该在方法上设置一个阻塞超时。超过超时意味着阻塞结束。

让我们调整之前的测试,使用这两个方法:

executorService.shutdown();
executorService.awaitTermination(10000, TimeUnit.SECONDS);
assertEquals(2305843005992468481L, r.getResult());

如您所见,我们在提交任务后关闭了执行器。接下来,我们调用awaitTermination()来让线程阻塞直到任务完成。

此外,我们设置了最大超时时间为10000秒。因此,如果任务运行超过10000秒,即使任务还没有结束,方法也会解除阻塞。换句话说,如果我们设置一个小的超时值,awaitTermination()会像Thread.sleep()一样过早地解除阻塞。

确实,当我们运行测试时,它成功了。

6. 使用ThreadPoolExecutor

另一个选项是创建一个接受预定数量任务并在它们完成时阻塞的ExecutorService对象

一个简单的方法是扩展ThreadPoolExecutor类:

public class MyThreadPoolExecutor extends ThreadPoolExecutor {
    CountDownLatch doneSignal = null;

    public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
        int jobsNumberToWaitFor) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        doneSignal = new CountDownLatch(jobsNumberToWaitFor);
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        doneSignal.countDown();
    }

    public void waitDone() throws InterruptedException {
        doneSignal.await();
    }
}

在这里,我们创建了MyThreadPoolExecutor类,它扩展了ThreadPoolExecutor。在构造函数中,我们添加了jobsNumberToWaitFor参数,这是我们计划提交的任务数量。

此外,类使用doneSignal字段,它是CountDownLatch类的一个实例。doneSignal字段在构造函数中初始化为等待的任务数量。接下来,我们重写了afterExecute()方法,将doneSignal减一。当一个任务结束时,会调用afterExecute()方法。

最后,我们有一个waitDone()方法,它使用doneSignal来阻塞直到所有任务结束。

此外,我们可以使用单元测试测试上述实现:

@Test
void whenUsingThreadPoolExecutor_thenTestSucceeds() throws InterruptedException {
    MyThreadPoolExecutor threadPoolExecutor = new MyThreadPoolExecutor(3, 6, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), 20);
    List<MyRunnable> runnables = new ArrayList<MyRunnable>();
    for (int i = 0; i < 20; i++) {
        MyRunnable r = new MyRunnable();
        runnables.add(r);
        threadPoolExecutor.submit(r);
    }
    threadPoolExecutor.waitDone();
    for (int i = 0; i < 20; i++) {
        assertEquals(2305843005992468481L, runnables.get(i).result);
    }
}

在这个单元测试中,我们向执行器提交20个任务。然后,我们立即调用waitDone()方法,阻塞直到20个任务完成。最后,我们断言每个任务的结果。

7. 总结

在这篇文章中,我们学习了如何不使用Thread.sleep()方法来单元测试ExecutorService实例。具体来说,我们探讨了三种方法:

  • 获取Future对象并调用get()方法
  • 关闭执行器并等待运行中的任务完成
  • 创建自定义ExecutorService

如往常一样,我们的示例的完整源代码可以在GitHub上找到。