Scalatest 异步测试套件与 Eventually 和 WhenReady (org.scalatest.concurrent)

Scalatest Asynchronous Test Suites vs Eventually and WhenReady (org.scalatest.concurrent)

我正在尝试使用 scalatest Asynchronous Test Suites,但除了对设置超时的一些限制之外,我看不到测试套件实际添加了什么。

我想知道是否有人精通 scalatest 的异步测试可以快速解释异步测试套件和 org.scalatest.concurrent 之间的区别。异步测试套件实际上添加了什么 org.scalatest.concurrent?一种方法比另一种更好吗?

我们比较了以下用于测试代码的 ScalaTest 工具 returns Futures:

异步风格特征

class AsyncSpec extends AsyncFlatSpec {
  ...
  Future(3).map { v => assert(v == 3) }
  ...
}
  • 非阻塞
  • 我们可以在 Future 完成之前断言,即 return Future[Assertion] 而不是 Assertion
  • 线程安全
  • 单线程串行执行上下文
  • Futures按照开始的顺序依次执行完成
  • 用于在测试主体中排队任务的同一线程也用于之后执行它们
  • 断言可以映射到 Futures
  • 无需在测试体内进行阻塞,即使用AwaitwhenReady
  • 消除了由于线程不足造成的片状问题
  • 测试主体中的最后一个表达式必须是 Future[Assertion]
  • 不支持测试主体中的多个断言
  • 不能在测试体内使用阻塞结构,因为它会因为等待而永远挂起测试 排队但从未开始任务

ScalaFutures

class ScalaFuturesSpec extends FlatSpec with ScalaFutures {
  ...
  whenReady(Future(3) { v => assert(v == 3) }
  ...
}
  • 阻塞
  • 我们必须等到完成 Future 才能 return Assertion
  • 不是线程安全的
  • 可能与全局执行上下文一起使用 scala.concurrent.ExecutionContext.Implicits.global 这是一个 用于并行执行的多线程池
  • 支持同一测试主体中的多个断言
  • 测试主体中的最后一个表达式不必是 Assertion

最终

class EventuallySpec extends FlatSpec with Eventually {
  ...
  eventually { assert(Future(3).value.contains(Success(3))) }
  ...
}
  • 更通用的设施不仅适用于 Futures
  • 这里的语义是重试按名称传递的任何类型的代码块,直到满足断言为止
  • 测试时 Futures 可能会使用全局执行上下文
  • 主要用于集成测试,其中针对响应时间不可预测的真实服务进行测试

单线程串行执行模型与线程池全局执行模型

scalatest-async-testing-comparison就是一个例子 展示两种执行模型的差异。

给出如下测试体

    val f1 = Future {
      val tmp = mutableSharedState
      Thread.sleep(5000)
      println(s"Start Future1 with mutableSharedState=$tmp in thread=${Thread.currentThread}")
      mutableSharedState = tmp + 1
      println(s"Complete Future1 with mutableSharedState=$mutableSharedState")
    }

    val f2 = Future {
      val tmp = mutableSharedState
      println(s"Start Future2 with mutableSharedState=$tmp in thread=${Thread.currentThread}")
      mutableSharedState = tmp + 1
      println(s"Complete Future2 with mutableSharedState=$mutableSharedState")
    }

    for {
      _ <- f1
      _ <- f2
    } yield {
      assert(mutableSharedState == 2)
    }

让我们考虑 AsyncSpecScalaFuturesSpec

的输出
  • 仅测试example.AsyncSpec:

    Start Future1 with mutableSharedState=0 in thread=Thread[pool-11-thread-3-ScalaTest-running-AsyncSpec,5,main]
    Complete Future1 with mutableSharedState=1
    Start Future2 with mutableSharedState=1 in thread=Thread[pool-11-thread-3-ScalaTest-running-AsyncSpec,5,main]
    Complete Future2 with mutableSharedState=2
    
  • 仅测试example.ScalaFuturesSpec:

    Start Future2 with mutableSharedState=0 in thread=Thread[scala-execution-context-global-119,5,main]
    Complete Future2 with mutableSharedState=1
    Start Future1 with mutableSharedState=0 in thread=Thread[scala-execution-context-global-120,5,main]
    Complete Future1 with mutableSharedState=1
    

请注意在串行执行模型中如何使用相同的线程并且 Futures 按顺序完成。另一方面, 在全局执行模型中使用了不同的线程,并且 Future2Future1 之前完成,这导致 共享可变状态的竞争条件,这反过来又导致测试失败。

我们应该使用哪一个 (IMO)?

在单元测试中,我们应该使用模拟子系统,其中 returned Futures 应该几乎立即完成,所以有 在单元测试中不需要 Eventually 。因此,选择是在异步样式和 ScalaFutures 之间。主要区别 两者之间的区别在于前者与后者不同是非阻塞的。如果可能的话,我们不应该阻止,所以我们 应该更喜欢像 AsyncFlatSpec 这样的异步样式。更大的区别是执行模型。异步样式 默认情况下使用自定义串行执行模型,它在共享可变状态上提供线程安全,与全局不同 线程池支持的执行模型通常与 ScalaFutures 一起使用。总之,我的建议是我们使用异步风格 除非我们有充分的理由不这样做。