我们比较以下 ScalaTest 工具来测试返回 Futures 的代码:
异步风格特征
class AsyncSpec extends AsyncFlatSpec {
...
Future(3).map { v => assert(v == 3) }
...
}
- 非阻塞
- 我们可以在
Future 完成之前断言,即返回Future[Assertion] 而不是Assertion
- 线程安全
- 单线程串行执行上下文
-
Futures 按启动顺序依次执行和完成
- 用于在测试主体中将任务排入队列的同一线程也用于之后执行它们
- 断言可以映射到
Futures
- 不需要在测试体内部进行阻塞,即使用
Await,whenReady
- 消除因线程不足而导致的片状现象
- 测试正文中的最后一个表达式必须是
Future[Assertion]
- 不支持测试体中的多个断言
- 不能在测试体内使用阻塞结构,因为它会因为等待而永远挂起测试
已入队但从未开始的任务
ScalaFutures
class ScalaFuturesSpec extends FlatSpec with ScalaFutures {
...
whenReady(Future(3) { v => assert(v == 3) }
...
}
- 阻止
- 我们必须等待完成
Future,然后才能返回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)
}
让我们考虑AsyncSpec 与ScalaFuturesSpec 的输出
-
testOnly 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
-
testOnly 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
注意在串行执行模型中如何使用相同的线程并按顺序完成 Future。另一方面,
在全局执行模型中使用了不同的线程,并且Future2在Future1之前完成,这导致
共享可变状态的竞争条件,进而导致测试失败。
我们应该使用哪一个 (IMO)?
在单元测试中,我们应该使用模拟子系统,其中返回的 Futures 应该几乎立即完成,所以有
在单元测试中不需要Eventually。因此,选择是在异步样式和ScalaFutures 之间。主要区别
两者之间的区别在于前者不像后者那样是非阻塞的。如果可能的话,我们不应该阻塞,所以我们
应该更喜欢 AsyncFlatSpec 这样的异步样式。更大的区别是执行模型。异步样式
默认情况下使用自定义串行执行模型,它提供共享可变状态的线程安全,不像全局
线程池支持的执行模型通常与ScalaFutures 一起使用。总之,我的建议是我们使用异步风格
除非我们有充分的理由不这样做。