如何在单元测试中避免使用 Thread.sleep?

23

让我们想象我有一个应该被测试的方法:

@Autowired
private RoutingService routingservice;

public void methodToBeTested() {
    Object objectToRoute = initializeObjectToRoute();
    if (someConditions) {
         routingService.routeInOneWay(objectToRoute);
    } else {
         routingService.routeInAnotherWay(objectToRoute);
    }
}
在这种情况下,RoutingService 运行在单独的线程中,因此在它的构造函数中我们有以下内容:
Thread thread = new Thread(this);
thread.setDaemon(true);
thread.start();
问题在于RoutingService更改了objectToRoute的状态,这正是我想要检查的,但这并不会立即发生,因此测试失败。但是,如果我添加Thread.sleep(),那么它就可以工作了,但这是不好的实践,我知道。在这种情况下,我该如何避免Thread.sleep()

1
在单元测试中使用 Thread.sleep 并不是一种不良实践,因为你只是模拟时间的流逝,这对于某些单元测试是必要的。人们之所以说使用 Thread.sleep 是一种不良实践,是因为有时它被用来尝试解决竞争条件。你只在测试中使用 Thread.sleep,还是在源代码中也使用了? - Ben Green
当您测试多线程代码时,如何确保您的单元测试不会出现意外情况?即它们总是以确定性方式执行。我认为模拟服务类是更好的选择。 - Anupam Saini
我只在测试中使用sleep,但发现很多人通常认为在单元测试中使用sleep并不好。例如,SonarLint插件也会抱怨并指出这是一种违规行为,并给出以下描述:“在测试中使用Thread.sleep通常是一个坏主意。它会创建脆弱的测试,这些测试可能因环境(“在我的机器上通过!”)或负载而无法预测地失败。” - Rufi
同意Ben的观点。我们在Maven构建期间运行JUnit测试,但当前线程不会等待多线程进程完成,例如JMS交互和外部性能监控。我们使用Thread.sleep来确保这些进程在线程被终止之前完成。 - MolonLabe
你不认为Sleep至少会使单元测试变慢吗?我不应该尽量避免它吗? - Rufi
显示剩余2条评论
7个回答

9

我建议使用 awaitility 来同步异步测试。例如,假设您有一个结果对象,在一些线程操作后被设置并且您想要对其进行测试。您可以编写以下语句:

 await()
.atMost(100, TimeUnit.SECONDS)
.untilAsserted(() -> assertNotNull(resultObject.getResult()));

它最多等待100秒,或者直到断言得到满足。例如,如果在0-100秒之间的某个时刻getResult()返回了某个非空值,执行会继续进行,而不像Thread.sleep一样无论结果是否存在都会暂停执行。


5
只是想知道这里的利润在哪里?除非我们只能赢得一些额外的时间,以便测试变得更快一些...但仍然类似于Thread.sleep,只不过带有一些语法糖。 - java_newbie
3
@java_newbie:从SonarLint规则描述(java:S2925):在测试中使用Thread.sleep通常是一个不好的想法。它会创建脆弱的测试,这些测试可能因环境(“在我的机器上通过!”)或负载而无法预测地失败。不要依赖于时间(使用模拟)或使用像Awaitility这样的库进行异步测试。 - jumping_monkey

8
如果您正在测试 methodToBeTested 方法的 [单元测试],那么您应该简单地模拟 routingservice。 您不应该测试 methodToBeTested 调用的任何方法。 然而,听起来您想测试 RoutingService (您说:“问题在于 RoutingService 更改了 objectToRoute 的状态,这正是我想要检查的”)。 要测试 RoutingService 方法,您应该为这些方法编写单独的单元测试。

是的,这可能是一种方式,但我不能说我喜欢它。最有可能的是我的测试从单元测试转移到集成测试。RoutingService已经过测试,但在这种情况下,我想验证在某些条件下是否进行了路由,但这可能需要一些时间。当然,我可以模拟RoutingService并验证调用了特定方法,但那样我将无法覆盖状态更改。 - Rufi
是的,如果你想这样做,那么它就成为了一个集成测试,并且不应该与正常的单元测试一起运行。单元测试应该快速而一致地完成,以便开发人员可以在提交任何代码之前运行所有测试。有一个单独的集成测试集,定期运行(例如通过Jenkins)。 - forgivenson

2

您可以模拟objectToRoute来设置一个CompletableFuture的值,然后在断言中调用get。这将等待值设置完成后再继续执行。然后设置一个超时时间@Test(timeout=5000)以防值永远不会被设置。

这样做的好处是测试不会等待更长的时间,而且很难因为时间太短而失败,因为你可以将超时时间设得比正常情况下更长。


0

这要看情况。正如Ben Green在他的评论中所说,睡眠在测试中是危险的,因为它可能隐藏了竞态条件。如果你知道整体设计包含这样的竞态条件,即服务可能在路由准备好之前就被使用,那么你应该在代码中修复它,例如通过测试一个“ready”条件,并在测试类中进行相同的测试。

如果你知道这种情况不会发生,你应该在你的主代码和测试类中记录下来。这将成为使用sleep的完美理由。

(我假设这是针对集成测试 - 对于单元测试,如其他答案中所说,mocks应该足够)


0
自从Java 9以后,你可以使用`CompleteableFuture`的`delayedExecutor`方法来避免使用`Thread.sleep`及其SonarLint警告。
以下是基本模式,请根据需要进行调整:
import static java.util.concurrent.CompletableFuture.delayedExecutor;
import static java.util.concurrent.CompletableFuture.runAsync;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

@Test
void yourTest() {

// [...]

  runAsync(() -> {}, delayedExecutor(100, MILLISECONDS)).join();
}

0

使用Object.wait()

我们已经处理了一些从目录或ZIP归档中获取文件的异步进程。我们在异步进程继续读取时在其他地方使用文件内容。

我们测试它们的方式是在通信对象上使用wait() --在我们的情况下是一个队列--,这样异步进程就会在有新文件准备好并继续工作时调用queue.notifyAll()。在另一端,消费者会逐个处理项目,直到队列为空,然后使用queue.wait()等待更多项目。我们确实使用通过队列传递的特殊对象来指示没有更多要处理的项目。

在您的情况下,我想要检查的对象objectToRoute并不是在您想要测试的方法内部真正创建的。您可以在测试中对其进行wait(),并在您想要测试的方法methodToBeTested内部对其进行notifyAll()。我知道这会在您的生产代码库中引入额外的代码行,但如果没有人在等待它,它应该是无害的。最终会变成以下形式:

public void methodToBeTested(Object objectToRoute) {
    if (someConditions) {
         routingService.routeInOneWay(objectToRoute);
    } else {
         routingService.routeInAnotherWay(objectToRoute);
    }
    synchronized(objectToRoute) {
        objectToRoute.notifyAll();
    }
}

在你的测试类中会有类似这样的代码:

@Test
public void testMethodToBeTested() throws InterruptedException {
    Object objectToRoute = initializeObjectToRoute();
    methodToBeTested(objectToRoute);
    synchronized (objectToRoute) {
        objectToRoute.wait();
    }
    verifyConditionsAfterRouting(objectToRoute);
}

我知道在这个简单的例子中并没有太多意义,因为示例系统不是多线程的。我假设多线程的扭曲是在routeInOneWayrouteInAnotherWay方法中添加的;因此,那些是调用notifyAll()方法的方法。

作为指向我们解决方案方向的示例代码,这里有一些代码片段。

在异步工作端或生产者端:

while(files.hasNext(){
   queue.add(files.next());
   synchronized (outputQueue) {
       queue.notifyAll()
   }
}

而在消费者方面:

while(!finished){
    while(!queue.isEmpty()){
        nextFile = queue.poll();
        if (nextFile.equals(NO_MORE_FILES_SIGNAL)) {
            finished = true;
            break;
        }
        doYourThingWith(nextFile);
    }
    if (!finished) {
        synchronized (outputQueue) {
            outputQueue.wait();
        }
    }
}

-3

不要避免使用Thread.sleep(),您可以在Junit测试用例中将值传递为零。


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