【问题标题】:Scalatest Asynchronous Test Suites vs Eventually and WhenReady (org.scalatest.concurrent)Scalatest 异步测试套件与最终和WhenReady (org.scalatest.concurrent)
【发布时间】:2019-08-24 13:12:36
【问题描述】:

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

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

【问题讨论】:

    标签: scala asynchronous future scalatest


    【解决方案1】:

    我们比较以下 ScalaTest 工具来测试返回 Futures 的代码:

    异步风格特征

    class AsyncSpec extends AsyncFlatSpec {
      ...
      Future(3).map { v => assert(v == 3) }
      ...
    }
    
    • 非阻塞
    • 我们可以在Future 完成之前断言,即返回Future[Assertion] 而不是Assertion
    • 线程安全
    • 单线程串行执行上下文
    • Futures 按启动顺序依次执行和完成
    • 用于在测试主体中将任务排入队列的同一线程也用于之后执行它们
    • 断言可以映射到Futures
    • 不需要在测试体内部进行阻塞,即使用AwaitwhenReady
    • 消除因线程不足而导致的片状现象
    • 测试正文中的最后一个表达式必须是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)
        }
    

    让我们考虑AsyncSpecScalaFuturesSpec 的输出

    • 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。另一方面, 在全局执行模型中使用了不同的线程,并且Future2Future1之前完成,这导致 共享可变状态的竞争条件,进而导致测试失败。

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

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

    【讨论】:

    猜你喜欢
    • 2017-07-19
    • 1970-01-01
    • 2017-07-20
    • 2017-05-22
    • 2012-07-15
    • 2021-04-20
    • 2018-03-06
    • 2015-05-30
    • 1970-01-01
    相关资源
    最近更新 更多