【问题标题】:How to avoid Thread.sleep in Unit tests?如何在单元测试中避免 Thread.sleep?
【发布时间】:2016-03-29 11:44:04
【问题描述】:

假设我有以下应该测试的方法:

@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()?

【问题讨论】:

  • 在单元测试中使用Thread.sleep 并不是一个坏习惯,因为您所做的只是模仿时间的流逝,这对于某些单元测试来说是必要的。人们说使用Thread.sleep 是一种不好的做法的原因是因为它有时被用作修复竞争条件的尝试。您是只在测试中使用Thread.sleep,还是在您的源代码中使用?
  • 当您测试线程代码时,您将如何确保您的单元测试不是片状的?他们总是以确定性的方式执行。我认为模拟服务类是一个更好的选择
  • 我只在测试中使用睡眠,但发现很多地方人们提到睡眠在单元测试中通常不是那么好。例如,SonarLint 插件也抱怨说它是违规的,并给出以下描述:“在测试中使用 Thread.sleep 通常是一个坏主意。它会创建脆弱的测试,可能会因环境而无法预测地失败(“传递我的机器!”)或加载。”
  • 同意本。我们在 Maven 构建期间运行 JUnit 测试,当前线程不会等待多线程进程,例如 JMS 交互和外部性能监控。 Thread.sleep 是我们用来确保这些进程在线程被杀死之前完成的。
  • 你不同意 Sleep 至少会使单元测试变慢吗?我不应该尽量避免吗?

标签: java multithreading unit-testing junit


【解决方案1】:

如果您正在为方法methodToBeTested 测试[单元测试],您应该简单地模拟routingservice。
您不应该测试methodToBeTested 调用的任何方法。

但是,听起来您想测试RoutingService(您说“问题是RoutingService 改变了objectToRoute 的状态,这正是我想要检查的”)。

要测试RoutingService 方法,您应该为这些方法编写单独的单元测试。

【讨论】:

  • 是的,这可能是一种方式,但是,我不能说我喜欢它。很可能我的测试从单元测试转移到集成测试。 RoutingService 已经过测试,但在这种情况下,我想验证在某些情况下路由是否已完成,但是,这可能需要一些时间。当然,我可以模拟 RoutingService 并验证是否调用了特定方法,但是我不会涵盖状态变化。
  • 是的,我会说如果你想这样做,它会变成一个集成测试,不应该与正常的单元测试集一起运行。单元测试应该快速且一致地完成,因此开发人员可以在签入任何代码之前运行它们。有一组单独的集成测试定期运行(例如通过 Jenkins)
【解决方案2】:

我建议awaitility 用于同步异步测试。例如,假设您有一个在某些线程操作后设置的结果对象,并且您想对其进行测试。你这样写声明:

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

它最多等待 100 秒,或直到断言得到满足。例如,如果 getResult() 在 0-100 秒之间返回非空值,则执行将继续,而 Thread.sleep 会在给定时间内保持执行,无论结果是否存在。

【讨论】:

  • 只是想知道这里的利润是多少?除非我们只能赢得一些额外的时间,所以测试会更快一些......但它仍然类似于 thread.sleep 只是有一些语法糖
【解决方案3】:

您可以模拟objectToRoute 来设置CompletableFuture 的值,然后在您的断言中调用get。这将等到值被设置后再继续。然后设置超时@Test(timeout=5000),以防该值从未设置。

这样做的好处是测试不会等待超过必要的时间,并且由于时间太短而更难失败,因为您可以使超时比正常情况大得多。

【讨论】:

    【解决方案4】:

    这取决于。正如 Ben Green 在他的评论中所说,睡眠在测试中是危险的,因为它可以隐藏比赛条件。 您知道整体设计是否包含这样的竞争条件,在路线准备好之前可以使用服务。确实如此,您应该在代码中修复它,例如通过测试 ready 条件,并在您的测试类中对其进行同样的测试。

    如果你知道它不可能发生,你应该在你的主代码中记录它和在你的测试类中。这将是睡觉的完美理由。

    (我认为这是用于集成测试 - 对于单元测试 mocks 应该就足够了,正如您在其他答案中所说的那样)

    【讨论】:

      【解决方案5】:

      使用 Object.wait()

      我们使用了一些从目录或 .ZIP 档案中获取文件的异步进程。我们在其他地方使用文件的内容,而异步的则继续读取。

      我们来测试它们的方法是在通信对象上使用wait()——在我们的例子中它是一个队列——,所以只要有一个新文件可供使用,异步进程就会queue.notifyAll()并继续工作。另一方面,消费者将一次处理一个项目,直到队列为空,然后queue.wait() 等待更多。我们确实使用了一个通过队列传递的特殊对象来指示没有更多的项目要处理。

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

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

      我知道在这个简单的示例中没有太大意义,因为示例系统不是多线程的。我假设在routeInOneWay 和routeInAnotherWay 方法中添加了多线程扭曲;因此,这些是调用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();
              }
          }
      }
      

      【讨论】:

        【解决方案6】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-01-18
          • 1970-01-01
          • 1970-01-01
          • 2016-11-22
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多